From 2076ac71f95f80cd6d06d0182ca521c6ab8db595 Mon Sep 17 00:00:00 2001 From: davidteather Date: Fri, 21 Jan 2022 00:27:45 -0600 Subject: [PATCH 01/17] Initial changes for v5 of the project. Initial changes for v5 of the project. Thanks @Daan-Grashoff for some of the helpers.extract_tag_contents code from a comment on #787 Co-Authored-By: Daan Grashoff <9222025+Daan-Grashoff@users.noreply.github.com> --- .github/workflows/package-test.yml | 2 +- TikTokApi/api/__init__.py | 0 TikTokApi/api/search.py | 109 ++++ TikTokApi/api/user.py | 196 ++++++++ TikTokApi/browser_utilities/browser.py | 6 +- TikTokApi/helpers.py | 23 + TikTokApi/tiktok.py | 670 +++++++------------------ setup.py | 1 + 8 files changed, 524 insertions(+), 483 deletions(-) create mode 100644 TikTokApi/api/__init__.py create mode 100644 TikTokApi/api/search.py create mode 100644 TikTokApi/api/user.py create mode 100644 TikTokApi/helpers.py diff --git a/.github/workflows/package-test.yml b/.github/workflows/package-test.yml index 9eb1f3a1..43d9d179 100644 --- a/.github/workflows/package-test.yml +++ b/.github/workflows/package-test.yml @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: os: [macos-latest] - python-version: [3.7, 3.9] + python-version: [3.7, 3.10] steps: - uses: actions/checkout@v2 - uses: microsoft/playwright-github-action@v1 diff --git a/TikTokApi/api/__init__.py b/TikTokApi/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/TikTokApi/api/search.py b/TikTokApi/api/search.py new file mode 100644 index 00000000..d16eae6b --- /dev/null +++ b/TikTokApi/api/search.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +from urllib.parse import quote, urlencode + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from ..tiktok import TikTokApi + +import logging +import requests + +class Search(): + parent: TikTokApi + + def __init__(self, parent: TikTokApi): + Search.parent = parent + + def users(self, search_term, count=28, **kwargs) -> list: + """Returns a list of users that match the search_term + + ##### Parameters + * search_term: The string to search for users by + This string is the term you want to search for users by. + + * count: The number of users to return + Note: maximum is around 28 for this type of endpoint. + """ + return self.discover_type(search_term, prefix="user", count=count, **kwargs) + + def music(self, search_term, count=28, **kwargs) -> list: + """Returns a list of music that match the search_term + + ##### Parameters + * search_term: The string to search for music by + This string is the term you want to search for music by. + + * count: The number of music to return + Note: maximum is around 28 for this type of endpoint. + """ + return self.discover_type(search_term, prefix="music", count=count, **kwargs) + + def hashtags(self, search_term, count=28, **kwargs) -> list: + """Returns a list of hashtags that match the search_term + + ##### Parameters + * search_term: The string to search for music by + This string is the term you want to search for music by. + + * count: The number of music to return + Note: maximum is around 28 for this type of endpoint. + """ + return self.discover_type( + search_term, prefix="challenge", count=count, **kwargs + ) + + def discover_type(self, search_term, prefix, count=28, offset=0, **kwargs) -> list: + """Returns a list of whatever the prefix type you pass in + + ##### Parameters + * search_term: The string to search by + + * prefix: The prefix of what to search for + + * count: The number search results to return + """ + ( + region, + language, + proxy, + maxCount, + device_id, + ) = self.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = device_id + + cursor = offset + + spawn = requests.head( + "https://www.tiktok.com", + proxies=self.parent._format_proxy(proxy), + **self.parent.requests_extra_kwargs + ) + ttwid = spawn.cookies["ttwid"] + + while cursor-offset < count: + query = { + "keyword": search_term, + "cursor": cursor, + "app_language": "en", + } + path = "api/search/{}/full/?{}&{}".format( + prefix, self.parent._add_url_params(), urlencode(query) + ) + + data = self.parent.get_data(path, use_desktop_base_url=True, ttwid=ttwid, **kwargs) + + # When I move to 3.10+ support make this a match switch. + if prefix == 'user': + cursor += len(data.get("user_list", [])) + for result in data.get("user_list", []): yield result + elif prefix == 'music': + cursor += len(data.get("music_list", [])) + for result in data.get("music_list", []): yield result + elif prefix == 'challenge': + cursor += len(data.get("challenge_list", [])) + for result in data.get("challenge_list", []): yield result + + if data.get('has_more', 0) == 0: + logging.info("TikTok is not sending videos beyond this point.") + return \ No newline at end of file diff --git a/TikTokApi/api/user.py b/TikTokApi/api/user.py new file mode 100644 index 00000000..ab0a4a17 --- /dev/null +++ b/TikTokApi/api/user.py @@ -0,0 +1,196 @@ +from __future__ import annotations +from concurrent.futures import process + +import json +import logging +import requests + +from typing import Optional +from urllib.parse import quote, urlencode + +from ..exceptions import * +from ..helpers import extract_tag_contents + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from ..tiktok import TikTokApi + +class User(): + parent: TikTokApi + + def __init__(self, username: Optional[str] = None, user_id: Optional[str] = None, sec_uid: Optional[str] = None): + super() + + self.username = username + self.user_id = user_id + self.sec_uid = sec_uid + + def user_object(self, **kwargs): + return self.data_full(**kwargs)['UserModule']['users'][self.username] + + def data_full(self, **kwargs) -> dict: + """Gets all data associated with the user.""" + + # TODO: Find the one using only user_id & sec_uid + if not self.username: + raise TypeError('You must provide the username when creating this class to use this method.') + + quoted_username = quote(self.username) + r = requests.get( + "https://tiktok.com/@{}?lang=en".format(quoted_username), + headers={ + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", + "path": "/@{}".format(quoted_username), + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36", + }, + proxies=User.parent._format_proxy(kwargs.get("proxy", None)), + cookies=User.parent.get_cookies(**kwargs), + **User.parent.requests_extra_kwargs, + ) + + data = extract_tag_contents(r.text) + user = json.loads(data) + + if user["UserPage"]["statusCode"] == 404: + raise TikTokNotFoundError("TikTok user with username {} does not exist".format(self.username)) + + return user + + def videos(self, count=30, cursor=0, **kwargs) -> dict: + """Returns an array of dictionaries representing TikToks for a user. + + ##### Parameters + * userID: The userID of the user, which TikTok assigns + + You can find this from utilizing other methods or + just use by_username to find it. + * secUID: The secUID of the user, which TikTok assigns + + You can find this from utilizing other methods or + just use by_username to find it. + * count: The number of posts to return + + Note: seems to only support up to ~2,000 + """ + ( + region, + language, + proxy, + maxCount, + device_id, + ) = User.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = device_id + + if not self.user_id and not self.sec_uid: + self.__find_attributes() + + first = True + amount_yielded = 0 + + while amount_yielded < count: + if count < maxCount: + realCount = count + else: + realCount = maxCount + + query = { + "count": realCount, + "id": self.user_id, + "cursor": cursor, + "type": 1, + "secUid": self.sec_uid, + "sourceType": 8, + "appId": 1233, + "region": region, + "priority_region": region, + "language": language, + } + path = "api/post/item_list/?{}&{}".format( + User.parent._add_url_params(), urlencode(query) + ) + + res = User.parent.get_data(path, send_tt_params=True, **kwargs) + + videos = res.get("itemList", []) + amount_yielded += len(videos) + for video in videos: yield video + + if not res.get("hasMore", False) and not first: + logging.info("TikTok isn't sending more TikToks beyond this point.") + return + + realCount = count - amount_yielded + cursor = res["cursor"] + first = False + + def liked(self, count: int = 30, cursor: int = 0, **kwargs): + """Returns a dictionary listing TikToks that a given a user has liked. + Note: The user's likes must be public + + ##### Parameters + * count: The number of posts to return + + Note: seems to only support up to ~2,000 + * cursor: The offset of a page + + The offset to return new videos from + """ + ( + region, + language, + proxy, + maxCount, + device_id, + ) = self._process_kwargs(kwargs) + kwargs["custom_device_id"] = device_id + + amount_yielded = 0 + first = True + + while amount_yielded < count: + if count < maxCount: + realCount = count + else: + realCount = maxCount + + query = { + "count": realCount, + "id": self.user_id, + "type": 2, + "secUid": self.sec_uid, + "cursor": cursor, + "sourceType": 9, + "appId": 1233, + "region": region, + "priority_region": region, + "language": language, + } + api_url = "api/favorite/item_list/?{}&{}".format( + User.parent.__add_url_params__(), urlencode(query) + ) + + res = self.get_data(url=api_url, **kwargs) + + if "itemList" not in res.keys(): + logging.error("User's likes are most likely private") + return + + videos = res.get("itemList", []) + amount_yielded += len(videos) + for video in videos: yield video + + if not res.get("hasMore", False) and not first: + logging.info("TikTok isn't sending more TikToks beyond this point.") + return + + realCount = count - amount_yielded + cursor = res["cursor"] + first = False + + def __find_attributes(self): + user_object = self.user_object() + self.user_id = user_object['id'] + self.sec_uid = user_object['secUid'] + self.username = user_object['uniqueId'] diff --git a/TikTokApi/browser_utilities/browser.py b/TikTokApi/browser_utilities/browser.py index 8e3b44ef..d4c3f297 100644 --- a/TikTokApi/browser_utilities/browser.py +++ b/TikTokApi/browser_utilities/browser.py @@ -173,14 +173,10 @@ def gen_verifyFp(self): return f'verify_{scenario_title.lower()}_{"".join(uuid)}' - def sign_url(self, calc_tt_params=False, **kwargs): + def sign_url(self, url, calc_tt_params=False, **kwargs): def process(route): route.abort() - url = kwargs.get("url", None) - if url is None: - raise Exception("sign_url required a url parameter") - tt_params = None context = self.create_context() page = context.new_page() diff --git a/TikTokApi/helpers.py b/TikTokApi/helpers.py new file mode 100644 index 00000000..16c0e074 --- /dev/null +++ b/TikTokApi/helpers.py @@ -0,0 +1,23 @@ +from TikTokApi.browser_utilities.browser import browser +from urllib.parse import quote, urlencode +from .exceptions import * + +import re + +def extract_tag_contents(html): + next_json = re.search(r'id=\"__NEXT_DATA__\"\s+type=\"application\/json\"\s*[^>]+>\s*(?P[^<]+)', html) + if (next_json): + nonce_start = '' + nonce = html.split(nonce_start)[1].split(nonce_end)[0] + j_raw = html.split( + '")[0] + return j_raw + else: + sigi_json = re.search(r'>\s*window\[[\'"]SIGI_STATE[\'"]\]\s*=\s*(?P{.+});', html) + if sigi_json: + return sigi_json.group(1) + else: + raise TikTokCaptchaError() \ No newline at end of file diff --git a/TikTokApi/tiktok.py b/TikTokApi/tiktok.py index 100ce1ce..0b61d7ce 100644 --- a/TikTokApi/tiktok.py +++ b/TikTokApi/tiktok.py @@ -7,88 +7,25 @@ from urllib.parse import quote, urlencode import requests +from TikTokApi.api import search from playwright.sync_api import sync_playwright from .exceptions import * from .utilities import update_messager +from .api import user + os.environ["no_proxy"] = "127.0.0.1,localhost" BASE_URL = "https://m.tiktok.com/" +DESKTOP_BASE_URL = "https://www.tiktok.com/" class TikTokApi: - __instance = None - - def __init__(self, **kwargs): - """The TikTokApi class. Used to interact with TikTok, use get_instance NOT this.""" - # Forces Singleton - if TikTokApi.__instance is None: - TikTokApi.__instance = self - else: - raise Exception("Only one TikTokApi object is allowed") - logging.basicConfig(level=kwargs.get("logging_level", logging.WARNING)) - logging.info("Class initalized") - - # Some Instance Vars - self.executablePath = kwargs.get("executablePath", None) - - if kwargs.get("custom_did") != None: - raise Exception("Please use custom_device_id instead of custom_device_id") - self.custom_device_id = kwargs.get("custom_device_id", None) - self.userAgent = ( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " - "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/86.0.4240.111 Safari/537.36" - ) - self.proxy = kwargs.get("proxy", None) - self.custom_verifyFp = kwargs.get("custom_verifyFp") - self.signer_url = kwargs.get("external_signer", None) - self.request_delay = kwargs.get("request_delay", None) - self.requests_extra_kwargs = kwargs.get("requests_extra_kwargs", {}) - - if kwargs.get("use_test_endpoints", False): - global BASE_URL - BASE_URL = "https://t.tiktok.com/" - if kwargs.get("use_selenium", False): - from .browser_utilities.browser_selenium import browser - else: - from .browser_utilities.browser import browser - - if kwargs.get("generate_static_device_id", False): - self.custom_device_id = "".join( - random.choice(string.digits) for num in range(19) - ) - - if self.signer_url is None: - self.browser = browser(**kwargs) - self.userAgent = self.browser.userAgent - - try: - self.timezone_name = self.__format_new_params__(self.browser.timezone_name) - self.browser_language = self.__format_new_params__( - self.browser.browser_language - ) - self.width = self.browser.width - self.height = self.browser.height - self.region = self.browser.region - self.language = self.browser.language - except Exception as e: - logging.exception(e) - logging.warning( - "An error ocurred while opening your browser but it was ignored." - ) - logging.warning("Are you sure you ran python -m playwright install") - - self.timezone_name = "" - self.browser_language = "" - self.width = "1920" - self.height = "1080" - self.region = "US" - self.language = "en" + _instance = None @staticmethod - def get_instance(**kwargs): + def __new__(cls, *args, **kwargs): """The TikTokApi class. Used to interact with TikTok. This is a singleton class to prevent issues from arising with playwright @@ -150,66 +87,80 @@ class to prevent issues from arising with playwright that interact with this main class. These may or may not be documented in other places. """ - if not TikTokApi.__instance: - TikTokApi(**kwargs) - return TikTokApi.__instance - def clean_up(self): - """A basic cleanup method, called automatically from the code""" - self.__del__() + if cls._instance is None: + cls._instance = super(TikTokApi, cls).__new__(cls) + cls._instance._initialize(*args, **kwargs) + return cls._instance - def __del__(self): - """A basic cleanup method, called automatically from the code""" - try: - self.browser.clean_up() - except Exception: - pass - try: - get_playwright().stop() - except Exception: - pass - TikTokApi.__instance = None + def _initialize(self, **kwargs): + # Add classes from the api folder + user.User.parent = self + self.user = user.User + self.search = search.Search(self) - def external_signer(self, url, custom_device_id=None, verifyFp=None): - """Makes requests to an external signer instead of using a browser. - ##### Parameters - * url: The server to make requests to - This server is designed to sign requests. You can find an example - of this signature server in the examples folder. + logging.basicConfig(level=kwargs.get("logging_level", logging.WARNING)) + logging.info("Class initalized") - * custom_device_id: A TikTok parameter needed to download videos - The code generates these and handles these pretty well itself, however - for some things such as video download you will need to set a consistent - one of these. + # Some Instance Vars + self.executablePath = kwargs.get("executablePath", None) - * custom_verifyFp: A TikTok parameter needed to work most of the time, - To get this parameter look at [this video](https://youtu.be/zwLmLfVI-VQ?t=117) - I recommend watching the entire thing, as it will help setup this package. - """ - if custom_device_id is not None: - query = { - "url": url, - "custom_device_id": custom_device_id, - "verifyFp": verifyFp, - } - else: - query = {"url": url, "verifyFp": verifyFp} - data = requests.get( - self.signer_url + "?{}".format(urlencode(query)), - **self.requests_extra_kwargs, + if kwargs.get("custom_did") != None: + raise Exception("Please use custom_device_id instead of custom_device_id") + self.custom_device_id = kwargs.get("custom_device_id", None) + self.userAgent = ( + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/86.0.4240.111 Safari/537.36" ) - parsed_data = data.json() + self.proxy = kwargs.get("proxy", None) + self.custom_verifyFp = kwargs.get("custom_verifyFp") + self.signer_url = kwargs.get("external_signer", None) + self.request_delay = kwargs.get("request_delay", None) + self.requests_extra_kwargs = kwargs.get("requests_extra_kwargs", {}) - return ( - parsed_data["verifyFp"], - parsed_data["device_id"], - parsed_data["_signature"], - parsed_data["userAgent"], - parsed_data["referrer"], - ) + if kwargs.get("use_test_endpoints", False): + global BASE_URL + BASE_URL = "https://t.tiktok.com/" + if kwargs.get("use_selenium", False): + from .browser_utilities.browser_selenium import browser + else: + from .browser_utilities.browser import browser - def get_data(self, **kwargs) -> dict: + if kwargs.get("generate_static_device_id", False): + self.custom_device_id = "".join( + random.choice(string.digits) for num in range(19) + ) + + if self.signer_url is None: + self.browser = browser(**kwargs) + self.userAgent = self.browser.userAgent + + try: + self.timezone_name = self.__format_new_params__(self.browser.timezone_name) + self.browser_language = self.__format_new_params__( + self.browser.browser_language + ) + self.width = self.browser.width + self.height = self.browser.height + self.region = self.browser.region + self.language = self.browser.language + except Exception as e: + logging.exception(e) + logging.warning( + "An error ocurred while opening your browser but it was ignored." + ) + logging.warning("Are you sure you ran python -m playwright install") + + self.timezone_name = "" + self.browser_language = "" + self.width = "1920" + self.height = "1080" + self.region = "US" + self.language = "en" + + def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict: """Makes requests to TikTok and returns their JSON. This is all handled by the package so it's unlikely @@ -221,7 +172,7 @@ def get_data(self, **kwargs) -> dict: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id if self.request_delay is not None: time.sleep(self.request_delay) @@ -240,16 +191,21 @@ def get_data(self, **kwargs) -> dict: tt_params = None send_tt_params = kwargs.get("send_tt_params", False) + if use_desktop_base_url: + full_url = DESKTOP_BASE_URL + path + else: + full_url = BASE_URL + path + if self.signer_url is None: kwargs["custom_verifyFp"] = verifyFp verify_fp, device_id, signature, tt_params = self.browser.sign_url( - calc_tt_params=send_tt_params, **kwargs + full_url, calc_tt_params=send_tt_params, **kwargs ) userAgent = self.browser.userAgent referrer = self.browser.referrer else: verify_fp, device_id, signature, userAgent, referrer = self.external_signer( - kwargs["url"], + full_url, custom_device_id=kwargs.get("custom_device_id"), verifyFp=kwargs.get("custom_verifyFp", verifyFp), ) @@ -258,17 +214,20 @@ def get_data(self, **kwargs) -> dict: tt_params = None query = {"verifyFp": verify_fp, "device_id": device_id, "_signature": signature} - url = "{}&{}".format(kwargs["url"], urlencode(query)) + url = "{}&{}".format(full_url, urlencode(query)) h = requests.head( url, headers={"x-secsdk-csrf-version": "1.2.5", "x-secsdk-csrf-request": "1"}, - proxies=self.__format_proxy(proxy), + proxies=self._format_proxy(proxy), **self.requests_extra_kwargs, ) - csrf_session_id = h.cookies["csrf_session_id"] - csrf_token = h.headers["X-Ware-Csrf-Token"].split(",")[1] - kwargs["csrf_session_id"] = csrf_session_id + + csrf_token = None + if not use_desktop_base_url: + csrf_session_id = h.cookies["csrf_session_id"] + csrf_token = h.headers["X-Ware-Csrf-Token"].split(",")[1] + kwargs["csrf_session_id"] = csrf_session_id headers = { "authority": "m.tiktok.com", @@ -276,13 +235,13 @@ def get_data(self, **kwargs) -> dict: "path": url.split("tiktok.com")[1], "scheme": "https", "accept": "application/json, text/plain, */*", - "accept-encoding": "gzip, deflate, br", + "accept-encoding": "gzip", "accept-language": "en-US,en;q=0.9", "origin": referrer, "referer": referrer, "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", - "sec-fetch-site": "same-site", + "sec-fetch-site": "none", "sec-gpc": "1", "user-agent": userAgent, "x-secsdk-csrf-token": csrf_token, @@ -294,9 +253,10 @@ def get_data(self, **kwargs) -> dict: url, headers=headers, cookies=self.get_cookies(**kwargs), - proxies=self.__format_proxy(proxy), + proxies=self._format_proxy(proxy), **self.requests_extra_kwargs, ) + try: json = r.json() if ( @@ -380,6 +340,61 @@ def get_data(self, **kwargs) -> dict: logging.error(e) raise JSONDecodeFailure() from e + def clean_up(self): + """A basic cleanup method, called automatically from the code""" + self.__del__() + + def __del__(self): + """A basic cleanup method, called automatically from the code""" + try: + self.browser.clean_up() + except Exception: + pass + try: + get_playwright().stop() + except Exception: + pass + TikTokApi._instance = None + + def external_signer(self, url, custom_device_id=None, verifyFp=None): + """Makes requests to an external signer instead of using a browser. + + ##### Parameters + * url: The server to make requests to + This server is designed to sign requests. You can find an example + of this signature server in the examples folder. + + * custom_device_id: A TikTok parameter needed to download videos + The code generates these and handles these pretty well itself, however + for some things such as video download you will need to set a consistent + one of these. + + * custom_verifyFp: A TikTok parameter needed to work most of the time, + To get this parameter look at [this video](https://youtu.be/zwLmLfVI-VQ?t=117) + I recommend watching the entire thing, as it will help setup this package. + """ + if custom_device_id is not None: + query = { + "url": url, + "custom_device_id": custom_device_id, + "verifyFp": verifyFp, + } + else: + query = {"url": url, "verifyFp": verifyFp} + data = requests.get( + self.signer_url + "?{}".format(urlencode(query)), + **self.requests_extra_kwargs, + ) + parsed_data = data.json() + + return ( + parsed_data["verifyFp"], + parsed_data["device_id"], + parsed_data["_signature"], + parsed_data["userAgent"], + parsed_data["referrer"], + ) + def get_cookies(self, **kwargs): """Extracts cookies from the kwargs passed to the function for get_data""" device_id = kwargs.get( @@ -426,7 +441,7 @@ def get_bytes(self, **kwargs) -> bytes: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id if self.signer_url is None: verify_fp, device_id, signature, _ = self.browser.sign_url( @@ -454,7 +469,7 @@ def get_bytes(self, **kwargs) -> bytes: "Referer": "https://www.tiktok.com/", "User-Agent": userAgent, }, - proxies=self.__format_proxy(proxy), + proxies=self._format_proxy(proxy), cookies=self.get_cookies(**kwargs), ) return r.content @@ -475,12 +490,12 @@ def by_trending(self, count=30, **kwargs) -> dict: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id spawn = requests.head( "https://www.tiktok.com", - proxies=self.__format_proxy(proxy), + proxies=self._format_proxy(proxy), **self.requests_extra_kwargs, ) ttwid = spawn.cookies["ttwid"] @@ -521,164 +536,6 @@ def by_trending(self, count=30, **kwargs) -> dict: return response[:count] - def search_for_users(self, search_term, count=28, **kwargs) -> list: - """Returns a list of users that match the search_term - - ##### Parameters - * search_term: The string to search for users by - This string is the term you want to search for users by. - - * count: The number of users to return - Note: maximum is around 28 for this type of endpoint. - """ - return self.discover_type(search_term, prefix="user", count=count, **kwargs) - - def search_for_music(self, search_term, count=28, **kwargs) -> list: - """Returns a list of music that match the search_term - - ##### Parameters - * search_term: The string to search for music by - This string is the term you want to search for music by. - - * count: The number of music to return - Note: maximum is around 28 for this type of endpoint. - """ - return self.discover_type(search_term, prefix="music", count=count, **kwargs) - - def search_for_hashtags(self, search_term, count=28, **kwargs) -> list: - """Returns a list of hashtags that match the search_term - - ##### Parameters - * search_term: The string to search for music by - This string is the term you want to search for music by. - - * count: The number of music to return - Note: maximum is around 28 for this type of endpoint. - """ - return self.discover_type( - search_term, prefix="challenge", count=count, **kwargs - ) - - def discover_type(self, search_term, prefix, count=28, offset=0, **kwargs) -> list: - """Returns a list of whatever the prefix type you pass in - - ##### Parameters - * search_term: The string to search by - - * prefix: The prefix of what to search for - - * count: The number search results to return - Note: maximum is around 28 for this type of endpoint. - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self.__process_kwargs__(kwargs) - kwargs["custom_device_id"] = device_id - - response = [] - while len(response) < count: - query = { - "discoverType": 0, - "needItemList": False, - "keyWord": search_term, - "offset": offset, - "count": count, - "useRecommend": False, - "language": "en", - } - api_url = "{}api/discover/{}/?{}&{}".format( - BASE_URL, prefix, self.__add_url_params__(), urlencode(query) - ) - data = self.get_data(url=api_url, **kwargs) - - if "userInfoList" in data.keys(): - for x in data["userInfoList"]: - response.append(x) - elif "musicInfoList" in data.keys(): - for x in data["musicInfoList"]: - response.append(x) - elif "challengeInfoList" in data.keys(): - for x in data["challengeInfoList"]: - response.append(x) - else: - logging.info("TikTok is not sending videos beyond this point.") - break - - offset += maxCount - - return response[:count] - - def user_posts(self, userID, secUID, count=30, cursor=0, **kwargs) -> dict: - """Returns an array of dictionaries representing TikToks for a user. - - ##### Parameters - * userID: The userID of the user, which TikTok assigns - - You can find this from utilizing other methods or - just use by_username to find it. - * secUID: The secUID of the user, which TikTok assigns - - You can find this from utilizing other methods or - just use by_username to find it. - * count: The number of posts to return - - Note: seems to only support up to ~2,000 - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self.__process_kwargs__(kwargs) - kwargs["custom_device_id"] = device_id - - response = [] - first = True - - while len(response) < count: - if count < maxCount: - realCount = count - else: - realCount = maxCount - - query = { - "count": realCount, - "id": userID, - "cursor": cursor, - "type": 1, - "secUid": secUID, - "sourceType": 8, - "appId": 1233, - "region": region, - "priority_region": region, - "language": language, - } - api_url = "{}api/post/item_list/?{}&{}".format( - BASE_URL, self.__add_url_params__(), urlencode(query) - ) - - res = self.get_data(url=api_url, send_tt_params=True, **kwargs) - - if "itemList" in res.keys(): - for t in res.get("itemList", []): - response.append(t) - - if not res.get("hasMore", False) and not first: - logging.info("TikTok isn't sending more TikToks beyond this point.") - return response - - realCount = count - len(response) - cursor = res["cursor"] - - first = False - - return response[:count] - def by_username(self, username, count=30, **kwargs) -> dict: """Returns a dictionary listing TikToks given a user's username. @@ -695,7 +552,7 @@ def by_username(self, username, count=30, **kwargs) -> dict: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id data = self.get_user_object(username, **kwargs) return self.user_posts( @@ -705,93 +562,6 @@ def by_username(self, username, count=30, **kwargs) -> dict: **kwargs, ) - def user_page(self, userID, secUID, page_size=30, cursor=0, **kwargs) -> dict: - """Returns a dictionary listing of one page of TikToks given a user's ID and secUID - - ##### Parameters - * userID: The userID of the user, which TikTok assigns - - You can find this from utilizing other methods or - just use by_username to find it. - * secUID: The secUID of the user, which TikTok assigns - - You can find this from utilizing other methods or - just use by_username to find it. - * page_size: The number of posts to return per page - - Gets a specific page of a user, doesn't iterate. - * cursor: The offset of a page - - The offset to return new videos from - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self.__process_kwargs__(kwargs) - kwargs["custom_device_id"] = device_id - - api_url = ( - BASE_URL + "api/post/item_list/?{}&count={}&id={}&type=1&secUid={}" - "&cursor={}&sourceType=8&appId=1233®ion={}&language={}".format( - self.__add_url_params__(), - page_size, - str(userID), - str(secUID), - cursor, - region, - language, - ) - ) - - return self.get_data(url=api_url, send_tt_params=True, **kwargs) - - def get_user_pager(self, username, page_size=30, cursor=0, **kwargs): - """Returns a generator to page through a user's feed - - ##### Parameters - * username: The username of the user - - * page_size: The number of posts to return in a page - - * cursor: The offset of a page - - The offset to return new videos from - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self.__process_kwargs__(kwargs) - kwargs["custom_device_id"] = device_id - data = self.get_user_object(username, **kwargs) - - while True: - resp = self.user_page( - data["id"], - data["secUid"], - page_size=page_size, - cursor=cursor, - **kwargs, - ) - - try: - page = resp["itemList"] - except KeyError: - # No mo results - return - - cursor = resp["cursor"] - - yield page - - if not resp["hasMore"]: - return # all done - def user_liked(self, userID, secUID, count=30, cursor=0, **kwargs) -> dict: """Returns a dictionary listing TikToks that a given a user has liked. Note: The user's likes must be public @@ -814,7 +584,7 @@ def user_liked(self, userID, secUID, count=30, cursor=0, **kwargs) -> dict: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id response = [] first = True @@ -880,7 +650,7 @@ def user_liked_by_username(self, username, count=30, **kwargs) -> dict: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id data = self.get_user_object(username, **kwargs) return self.user_liked( @@ -907,7 +677,7 @@ def by_sound(self, id, count=30, offset=0, **kwargs) -> dict: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id response = [] @@ -963,7 +733,7 @@ def by_sound_page(self, id, page_size=30, cursor=0, **kwargs) -> dict: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id query = { @@ -1002,7 +772,7 @@ def get_music_object_full(self, id, **kwargs): proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) r = requests.get( "https://www.tiktok.com/music/-{}".format(id), headers={ @@ -1011,7 +781,7 @@ def get_music_object_full(self, id, **kwargs): "Connection": "keep-alive", "User-Agent": self.userAgent, }, - proxies=self.__format_proxy(kwargs.get("proxy", None)), + proxies=self._format_proxy(kwargs.get("proxy", None)), cookies=self.get_cookies(**kwargs), **self.requests_extra_kwargs, ) @@ -1033,7 +803,7 @@ def get_music_object_full_by_api(self, id, **kwargs): proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id api_url = "{}node/share/music/-{}?{}".format( @@ -1064,7 +834,7 @@ def by_hashtag(self, hashtag, count=30, offset=0, **kwargs) -> dict: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id id = self.get_hashtag_object(hashtag)["challengeInfo"]["challenge"]["id"] response = [] @@ -1112,7 +882,7 @@ def get_hashtag_object(self, hashtag, **kwargs) -> dict: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id query = {"name": hashtag, "isName": True, "lang": language} api_url = "{}node/share/tag/{}?{}&{}".format( @@ -1139,7 +909,7 @@ def get_recommended_tiktoks_by_video_id(self, id, count=30, **kwargs) -> dict: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id response = [] @@ -1192,7 +962,7 @@ def get_tiktok_by_id(self, id, **kwargs) -> dict: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id device_id = kwargs.get("custom_device_id", None) query = { @@ -1222,7 +992,7 @@ def get_tiktok_by_url(self, url, **kwargs) -> dict: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id custom_device_id = kwargs.get("custom_device_id", None) if "@" in url and "/video/" in url: @@ -1251,7 +1021,7 @@ def get_tiktok_by_html(self, url, **kwargs) -> dict: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) r = requests.get( url, @@ -1262,7 +1032,7 @@ def get_tiktok_by_html(self, url, **kwargs) -> dict: "Connection": "keep-alive", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36", }, - proxies=self.__format_proxy(kwargs.get("proxy", None)), + proxies=self._format_proxy(kwargs.get("proxy", None)), cookies=self.get_cookies(**kwargs), **self.requests_extra_kwargs, ) @@ -1296,56 +1066,11 @@ def get_user_object(self, username, **kwargs) -> dict: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id return self.get_user(username, **kwargs)["userInfo"]["user"] - def get_user(self, username, **kwargs) -> dict: - """Gets the full exposed user object - - ##### Parameters - * username: The username of the user - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self.__process_kwargs__(kwargs) - r = requests.get( - "https://tiktok.com/@{}?lang=en".format(quote(username)), - headers={ - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", - "path": "/@{}".format(quote(username)), - "Accept-Encoding": "gzip, deflate", - "Connection": "keep-alive", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36", - }, - proxies=self.__format_proxy(kwargs.get("proxy", None)), - cookies=self.get_cookies(**kwargs), - **self.requests_extra_kwargs, - ) - - t = r.text - - try: - j_raw = self.__extract_tag_contents(r.text) - except IndexError: - if not t: - logging.error("Tiktok response is empty") - else: - logging.error("Tiktok response: \n " + t) - raise TikTokCaptchaError() - - user = json.loads(j_raw)["props"]["pageProps"] - - if user["serverCode"] == 404: - raise TikTokNotFoundError( - "TikTok user with username {} does not exist".format(username) - ) - - return user + def get_video_by_tiktok(self, data, **kwargs) -> bytes: """Downloads video from TikTok using a TikTok object. @@ -1366,7 +1091,7 @@ def get_video_by_tiktok(self, data, **kwargs) -> bytes: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id try: api_url = data["video"]["downloadAddr"] @@ -1389,7 +1114,7 @@ def get_video_by_download_url(self, download_url, **kwargs) -> bytes: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id return self.get_bytes(url=download_url, **kwargs) @@ -1405,7 +1130,7 @@ def get_video_by_url(self, video_url, **kwargs) -> bytes: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id tiktok_schema = self.get_tiktok_by_url(video_url, **kwargs) @@ -1430,7 +1155,7 @@ def get_video_no_watermark(self, video_url, return_bytes=1, **kwargs) -> bytes: proxy, maxCount, device_id, - ) = self.__process_kwargs__(kwargs) + ) = self._process_kwargs(kwargs) raise Exception("Deprecated method, TikTok fixed this.") def get_music_title(self, id, **kwargs): @@ -1447,7 +1172,7 @@ def get_music_title(self, id, **kwargs): "Connection": "keep-alive", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36", }, - proxies=self.__format_proxy(kwargs.get("proxy", None)), + proxies=self._format_proxy(kwargs.get("proxy", None)), cookies=self.get_cookies(**kwargs), **self.requests_extra_kwargs, ) @@ -1475,7 +1200,7 @@ def get_secuid(self, username, **kwargs): "Connection": "keep-alive", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36", }, - proxies=self.__format_proxy( + proxies=self._format_proxy( kwargs.get("proxy", None), cookies=self.get_cookies(**kwargs) ), **self.requests_extra_kwargs, @@ -1498,7 +1223,7 @@ def generate_device_id(): # PRIVATE METHODS # - def __format_proxy(self, proxy) -> dict: + def _format_proxy(self, proxy) -> dict: """ Formats the proxy object """ @@ -1509,10 +1234,27 @@ def __format_proxy(self, proxy) -> dict: else: return None + # Process the kwargs + def _process_kwargs(self, kwargs): + region = kwargs.get("region", "US") + language = kwargs.get("language", "en") + proxy = kwargs.get("proxy", None) + maxCount = kwargs.get("maxCount", 35) + + if kwargs.get("custom_device_id", None) != None: + device_id = kwargs.get("custom_device_id") + else: + if self.custom_device_id != None: + device_id = self.custom_device_id + else: + device_id = "".join(random.choice(string.digits) for num in range(19)) + return region, language, proxy, maxCount, device_id + + def __get_js(self, proxy=None) -> str: return requests.get( "https://sf16-muse-va.ibytedtos.com/obj/rc-web-sdk-gcs/acrawler.js", - proxies=self.__format_proxy(proxy), + proxies=self._format_proxy(proxy), **self.requests_extra_kwargs, ).text @@ -1520,7 +1262,7 @@ def __format_new_params__(self, parm) -> str: # TODO: Maybe try not doing this? It should be handled by the urlencode. return parm.replace("/", "%2F").replace(" ", "+").replace(";", "%3B") - def __add_url_params__(self) -> str: + def _add_url_params(self) -> str: query = { "aid": 1988, "app_name": "tiktok_web", @@ -1546,29 +1288,3 @@ def __add_url_params__(self) -> str: "language": self.language or "en", } return urlencode(query) - - def __extract_tag_contents(self, html): - nonce_start = '' - nonce = html.split(nonce_start)[1].split(nonce_end)[0] - j_raw = html.split( - '")[0] - return j_raw - - # Process the kwargs - def __process_kwargs__(self, kwargs): - region = kwargs.get("region", "US") - language = kwargs.get("language", "en") - proxy = kwargs.get("proxy", None) - maxCount = kwargs.get("maxCount", 35) - - if kwargs.get("custom_device_id", None) != None: - device_id = kwargs.get("custom_device_id") - else: - if self.custom_device_id != None: - device_id = self.custom_device_id - else: - device_id = "".join(random.choice(string.digits) for num in range(19)) - return region, language, proxy, maxCount, device_id diff --git a/setup.py b/setup.py index e7932834..ab0138d6 100644 --- a/setup.py +++ b/setup.py @@ -27,5 +27,6 @@ "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", ], ) From a505538ddae19417ac9354953a9330f3ae985758 Mon Sep 17 00:00:00 2001 From: davidteather Date: Fri, 21 Jan 2022 15:45:46 -0600 Subject: [PATCH 02/17] finish separating classes --- TikTokApi/api/hashtag.py | 95 +++ TikTokApi/api/music.py | 101 +++ TikTokApi/api/search.py | 9 +- TikTokApi/api/trending.py | 68 ++ TikTokApi/api/user.py | 35 +- TikTokApi/api/video.py | 59 ++ TikTokApi/browser_utilities/browser.py | 2 +- .../browser_utilities/browser_selenium.py | 2 +- TikTokApi/helpers.py | 13 +- TikTokApi/tiktok.py | 782 +----------------- docs/TikTokApi/tiktok.html | 20 +- 11 files changed, 395 insertions(+), 791 deletions(-) create mode 100644 TikTokApi/api/hashtag.py create mode 100644 TikTokApi/api/music.py create mode 100644 TikTokApi/api/trending.py create mode 100644 TikTokApi/api/video.py diff --git a/TikTokApi/api/hashtag.py b/TikTokApi/api/hashtag.py new file mode 100644 index 00000000..1b8b83c1 --- /dev/null +++ b/TikTokApi/api/hashtag.py @@ -0,0 +1,95 @@ +from __future__ import annotations +import logging + +from urllib.parse import urlencode +from ..exceptions import * + +from typing import TYPE_CHECKING, Optional +if TYPE_CHECKING: + from ..tiktok import TikTokApi + +class Hashtag(): + parent: TikTokApi + + def __init__(self, name: Optional[str] = None, id: Optional[str] = None): + self.name = name + self.id = id + + def challenge_info(self, **kwargs) -> dict: + return self.data_full(**kwargs)['challengeInfo']['challenge'] + + def data_full(self, **kwargs) -> dict: + """Returns a hashtag object. + + ##### Parameters + * hashtag: The hashtag to search by + + Without the # symbol + """ + ( + region, + language, + proxy, + maxCount, + device_id, + ) = self.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = device_id + + if self.name: + query = {"challengeName": self.name} + else: + query = {"challengeId": self.id} + path = "api/challenge/detail/?{}&{}".format( + self.parent._add_url_params(), urlencode(query) + ) + + data = self.parent.get_data(path, **kwargs) + + if data["challengeInfo"].get("challenge") is None: + raise TikTokNotFoundError("Challenge {} does not exist".format(self.name)) + + return data + + def videos(self, count=30, offset=0, **kwargs) -> dict: + """Returns a dictionary listing TikToks with a specific hashtag. + + ##### Parameters + * count: The number of posts to return + Note: seems to only support up to ~2,000 + """ + ( + region, + language, + proxy, + maxCount, + device_id, + ) = self.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = device_id + + if not self.id: + self.id = self.challenge_info()['id'] + + cursor = offset + page_size = 30 + + while cursor-offset < count: + query = { + "count": page_size, + "challengeID": self.id, + "type": 3, + "secUid": "", + "cursor": cursor, + "priority_region": "", + } + path = "api/challenge/item_list/?{}&{}".format( + self.parent._add_url_params(), urlencode(query) + ) + res = self.parent.get_data(path, **kwargs) + + for result in res.get("itemList", []): yield result + + if not res.get("hasMore", False): + logging.info("TikTok isn't sending more TikToks beyond this point.") + return + + cursor = int(res["cursor"]) diff --git a/TikTokApi/api/music.py b/TikTokApi/api/music.py new file mode 100644 index 00000000..c7ef40f4 --- /dev/null +++ b/TikTokApi/api/music.py @@ -0,0 +1,101 @@ +from __future__ import annotations +from os import path + +import requests +import logging +import json + +from urllib.parse import quote, urlencode + +from ..helpers import extract_tag_contents +from ..exceptions import * + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from ..tiktok import TikTokApi + + +class Music(): + parent: TikTokApi + + def __init__(self, music_id: str): + self.id = music_id + + def music_info(self, use_html=False, **kwargs) -> dict: + if use_html: + return self.data_full(**kwargs)['props']['pageProps']['musicInfo'] + + ( + region, + language, + proxy, + maxCount, + device_id, + ) = self.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = device_id + + path = "node/share/music/-{}?{}".format( + self.id, self.parent._add_url_params() + ) + res = self.parent.get_data(path, **kwargs) + + if res.get("statusCode", 200) == 10203: + raise TikTokNotFoundError() + + return res['musicInfo'] + + def data_full(self, **kwargs) -> dict: + r = requests.get( + "https://www.tiktok.com/music/-{}".format(self.id), + headers={ + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "User-Agent": self.parent.user_agent, + }, + proxies=self.parent._format_proxy(kwargs.get("proxy", None)), + cookies=self.parent.get_cookies(**kwargs), + **self.parent.requests_extra_kwargs, + ) + + data = extract_tag_contents(r.text) + return json.loads(data) + + def videos(self, count=30, offset=0, **kwargs) -> dict: + """Returns a dictionary listing TikToks with a specific sound. + + Note: seems to only support up to ~2,000 + """ + ( + region, + language, + proxy, + maxCount, + device_id, + ) = self.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = device_id + + cursor = offset + page_size = 30 + + while cursor-offset < count: + query = { + "secUid": "", + "musicID": self.id, + "cursor": cursor, + "shareUid": "", + "count": page_size + } + path = "api/music/item_list/?{}&{}".format( + self.parent._add_url_params(), urlencode(query) + ) + + res = self.parent.get_data(path, send_tt_params=True, **kwargs) + + for result in res.get("itemList", []): yield result + + if not res.get("hasMore", False): + logging.info("TikTok isn't sending more TikToks beyond this point.") + return + + cursor = int(res["cursor"]) \ No newline at end of file diff --git a/TikTokApi/api/search.py b/TikTokApi/api/search.py index d16eae6b..763c383b 100644 --- a/TikTokApi/api/search.py +++ b/TikTokApi/api/search.py @@ -1,6 +1,6 @@ from __future__ import annotations -from urllib.parse import quote, urlencode +from urllib.parse import urlencode from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -95,15 +95,14 @@ def discover_type(self, search_term, prefix, count=28, offset=0, **kwargs) -> li # When I move to 3.10+ support make this a match switch. if prefix == 'user': - cursor += len(data.get("user_list", [])) for result in data.get("user_list", []): yield result elif prefix == 'music': - cursor += len(data.get("music_list", [])) for result in data.get("music_list", []): yield result elif prefix == 'challenge': - cursor += len(data.get("challenge_list", [])) for result in data.get("challenge_list", []): yield result if data.get('has_more', 0) == 0: logging.info("TikTok is not sending videos beyond this point.") - return \ No newline at end of file + return + + cursor = int(data.get('cursor')) \ No newline at end of file diff --git a/TikTokApi/api/trending.py b/TikTokApi/api/trending.py new file mode 100644 index 00000000..3e622894 --- /dev/null +++ b/TikTokApi/api/trending.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +import logging +import requests +from urllib.parse import urlencode + + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from ..tiktok import TikTokApi + + +class Trending(): + parent: TikTokApi + + @staticmethod + def videos(count=30, **kwargs) -> dict: + """ + Gets trending TikToks + + ##### Parameters + * count: The amount of TikToks you want returned, optional + + Note: TikTok seems to only support at MOST ~2000 TikToks + from a single endpoint. + """ + ( + region, + language, + proxy, + maxCount, + device_id, + ) = Trending.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = device_id + + spawn = requests.head( + "https://www.tiktok.com", + proxies=Trending.parent._format_proxy(proxy), + **Trending.parent.requests_extra_kwargs, + ) + ttwid = spawn.cookies["ttwid"] + + first = True + amount_yielded = 0 + + while amount_yielded < count: + query = { + "count": 30, + "id": 1, + "sourceType": 12, + "itemID": 1, + "insertedItemID": "", + "region": region, + "priority_region": region, + "language": language, + } + path = "api/recommend/item_list/?{}&{}".format( + Trending.parent._add_url_params(), urlencode(query) + ) + res = Trending.parent.get_data(path, ttwid=ttwid, **kwargs) + for result in res.get("itemList", []): yield result + amount_yielded += len(res.get("itemList", [])) + + if not res.get("hasMore", False) and not first: + logging.info("TikTok isn't sending more TikToks beyond this point.") + return + + first = False \ No newline at end of file diff --git a/TikTokApi/api/user.py b/TikTokApi/api/user.py index ab0a4a17..3afdbf9a 100644 --- a/TikTokApi/api/user.py +++ b/TikTokApi/api/user.py @@ -5,13 +5,13 @@ import logging import requests -from typing import Optional from urllib.parse import quote, urlencode from ..exceptions import * from ..helpers import extract_tag_contents +from .search import Search -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from ..tiktok import TikTokApi @@ -19,8 +19,6 @@ class User(): parent: TikTokApi def __init__(self, username: Optional[str] = None, user_id: Optional[str] = None, sec_uid: Optional[str] = None): - super() - self.username = username self.user_id = user_id self.sec_uid = sec_uid @@ -43,7 +41,7 @@ def data_full(self, **kwargs) -> dict: "path": "/@{}".format(quoted_username), "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36", + "User-Agent": self.parent.user_agent, }, proxies=User.parent._format_proxy(kwargs.get("proxy", None)), cookies=User.parent.get_cookies(**kwargs), @@ -62,14 +60,6 @@ def videos(self, count=30, cursor=0, **kwargs) -> dict: """Returns an array of dictionaries representing TikToks for a user. ##### Parameters - * userID: The userID of the user, which TikTok assigns - - You can find this from utilizing other methods or - just use by_username to find it. - * secUID: The secUID of the user, which TikTok assigns - - You can find this from utilizing other methods or - just use by_username to find it. * count: The number of posts to return Note: seems to only support up to ~2,000 @@ -190,7 +180,18 @@ def liked(self, count: int = 30, cursor: int = 0, **kwargs): first = False def __find_attributes(self): - user_object = self.user_object() - self.user_id = user_object['id'] - self.sec_uid = user_object['secUid'] - self.username = user_object['uniqueId'] + # It is more efficient to check search first, since self.user_object() makes HTML request. + user_object = None + for r in Search(self.parent).users(self.username): + if r['user_info']['unique_id'] == self.username: + user_object = r['user_info'] + self.user_id = user_object['uid'] + self.sec_uid = user_object['sec_uid'] + self.username = user_object['unique_id'] + break + + if user_object is None: + user_object = self.user_object() + self.user_id = user_object['id'] + self.sec_uid = user_object['secUid'] + self.username = user_object['uniqueId'] diff --git a/TikTokApi/api/video.py b/TikTokApi/api/video.py new file mode 100644 index 00000000..2c653791 --- /dev/null +++ b/TikTokApi/api/video.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from urllib.parse import urlencode + +from ..helpers import extract_video_id_from_url + +from typing import TYPE_CHECKING, Optional +if TYPE_CHECKING: + from ..tiktok import TikTokApi + +class Video(): + parent: TikTokApi + + def __init__(self, id: Optional[str] = None, url: Optional[str] = None, raw: Optional[dict] = None): + if id: + self.id = id + elif url: + self.id = extract_video_id_from_url(url) + else: + raise TypeError("You must provide id or url parameter.") + + def video_info(self, **kwargs) -> dict: + return self.data_full(**kwargs)['itemInfo']['itemStruct'] + + def data_full(self, **kwargs) -> dict: + """Returns a dictionary of a specific TikTok.""" + ( + region, + language, + proxy, + maxCount, + device_id, + ) = self.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = device_id + + device_id = kwargs.get("custom_device_id", None) + query = { + "itemId": self.id, + } + path = "api/item/detail/?{}&{}".format( + self.parent._add_url_params(), urlencode(query) + ) + + return self.parent.get_data(path, **kwargs) + + def bytes(self, **kwargs) -> bytes: + ( + region, + language, + proxy, + maxCount, + device_id, + ) = self.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = device_id + + video_data = self.video_info(**kwargs) + download_url = video_data['video']['playAddr'] + + return self.parent.get_bytes(url=download_url, **kwargs) \ No newline at end of file diff --git a/TikTokApi/browser_utilities/browser.py b/TikTokApi/browser_utilities/browser.py index d4c3f297..c162239c 100644 --- a/TikTokApi/browser_utilities/browser.py +++ b/TikTokApi/browser_utilities/browser.py @@ -132,7 +132,7 @@ def create_context(self, set_useragent=False): context = self.browser.new_context(**iphone) if set_useragent: - self.userAgent = iphone["user_agent"] + self.user_agent = iphone["user_agent"] return context diff --git a/TikTokApi/browser_utilities/browser_selenium.py b/TikTokApi/browser_utilities/browser_selenium.py index 1b79df91..e76ec0c4 100644 --- a/TikTokApi/browser_utilities/browser_selenium.py +++ b/TikTokApi/browser_utilities/browser_selenium.py @@ -92,7 +92,7 @@ def setup_browser(self): self.browser.execute_script(get_tt_params_script()) def get_params(self, page) -> None: - self.userAgent = page.execute_script("""return navigator.userAgent""") + self.user_agent = page.execute_script("""return navigator.userAgent""") self.browser_language = self.kwargs.get( "browser_language", ("""return navigator.language""") ) diff --git a/TikTokApi/helpers.py b/TikTokApi/helpers.py index 16c0e074..d835e009 100644 --- a/TikTokApi/helpers.py +++ b/TikTokApi/helpers.py @@ -3,6 +3,7 @@ from .exceptions import * import re +import requests def extract_tag_contents(html): next_json = re.search(r'id=\"__NEXT_DATA__\"\s+type=\"application\/json\"\s*[^>]+>\s*(?P[^<]+)', html) @@ -20,4 +21,14 @@ def extract_tag_contents(html): if sigi_json: return sigi_json.group(1) else: - raise TikTokCaptchaError() \ No newline at end of file + raise TikTokCaptchaError() + +def extract_video_id_from_url(url): + url = requests.head(url=url, allow_redirects=True).url + if "@" in url and "/video/" in url: + post_id = url.split("/video/")[1].split("?")[0] + else: + raise TypeError( + "URL format not supported. Below is an example of a supported url.\n" + "https://www.tiktok.com/@therock/video/6829267836783971589" + ) \ No newline at end of file diff --git a/TikTokApi/tiktok.py b/TikTokApi/tiktok.py index 0b61d7ce..6c2b09d8 100644 --- a/TikTokApi/tiktok.py +++ b/TikTokApi/tiktok.py @@ -7,14 +7,12 @@ from urllib.parse import quote, urlencode import requests -from TikTokApi.api import search +from TikTokApi.api import user, music, search, hashtag, video, trending from playwright.sync_api import sync_playwright from .exceptions import * from .utilities import update_messager -from .api import user - os.environ["no_proxy"] = "127.0.0.1,localhost" BASE_URL = "https://m.tiktok.com/" @@ -97,8 +95,20 @@ def _initialize(self, **kwargs): # Add classes from the api folder user.User.parent = self self.user = user.User + self.search = search.Search(self) + music.Music.parent = self + self.music = music.Music + + hashtag.Hashtag.parent = self + self.hashtag = hashtag.Hashtag + + video.Video.parent = self + self.video = video.Video + + trending.Trending.parent = self + self.trending = trending.Trending logging.basicConfig(level=kwargs.get("logging_level", logging.WARNING)) logging.info("Class initalized") @@ -109,7 +119,7 @@ def _initialize(self, **kwargs): if kwargs.get("custom_did") != None: raise Exception("Please use custom_device_id instead of custom_device_id") self.custom_device_id = kwargs.get("custom_device_id", None) - self.userAgent = ( + self.user_agent = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/86.0.4240.111 Safari/537.36" @@ -135,11 +145,11 @@ def _initialize(self, **kwargs): if self.signer_url is None: self.browser = browser(**kwargs) - self.userAgent = self.browser.userAgent + self.user_agent = self.browser.user_agent try: - self.timezone_name = self.__format_new_params__(self.browser.timezone_name) - self.browser_language = self.__format_new_params__( + self.timezone_name = self.__format_new_params(self.browser.timezone_name) + self.browser_language = self.__format_new_params( self.browser.browser_language ) self.width = self.browser.width @@ -201,10 +211,10 @@ def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict: verify_fp, device_id, signature, tt_params = self.browser.sign_url( full_url, calc_tt_params=send_tt_params, **kwargs ) - userAgent = self.browser.userAgent + user_agent = self.browser.user_agent referrer = self.browser.referrer else: - verify_fp, device_id, signature, userAgent, referrer = self.external_signer( + verify_fp, device_id, signature, user_agent, referrer = self.external_signer( full_url, custom_device_id=kwargs.get("custom_device_id"), verifyFp=kwargs.get("custom_verifyFp", verifyFp), @@ -243,7 +253,7 @@ def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict: "sec-fetch-mode": "cors", "sec-fetch-site": "none", "sec-gpc": "1", - "user-agent": userAgent, + "user-agent": user_agent, "x-secsdk-csrf-token": csrf_token, "x-tt-params": tt_params, } @@ -391,7 +401,7 @@ def external_signer(self, url, custom_device_id=None, verifyFp=None): parsed_data["verifyFp"], parsed_data["device_id"], parsed_data["_signature"], - parsed_data["userAgent"], + parsed_data["user_agent"], parsed_data["referrer"], ) @@ -447,10 +457,10 @@ def get_bytes(self, **kwargs) -> bytes: verify_fp, device_id, signature, _ = self.browser.sign_url( calc_tt_params=False, **kwargs ) - userAgent = self.browser.userAgent + user_agent = self.browser.user_agent referrer = self.browser.referrer else: - verify_fp, device_id, signature, userAgent, referrer = self.external_signer( + verify_fp, device_id, signature, user_agent, referrer = self.external_signer( kwargs["url"], custom_device_id=kwargs.get("custom_device_id", None) ) query = {"verifyFp": verify_fp, "_signature": signature} @@ -467,753 +477,13 @@ def get_bytes(self, **kwargs) -> bytes: "Pragma": "no-cache", "Range": "bytes=0-", "Referer": "https://www.tiktok.com/", - "User-Agent": userAgent, + "User-Agent": user_agent, }, proxies=self._format_proxy(proxy), cookies=self.get_cookies(**kwargs), ) return r.content - def by_trending(self, count=30, **kwargs) -> dict: - """ - Gets trending TikToks - - ##### Parameters - * count: The amount of TikToks you want returned, optional - - Note: TikTok seems to only support at MOST ~2000 TikToks - from a single endpoint. - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id - - spawn = requests.head( - "https://www.tiktok.com", - proxies=self._format_proxy(proxy), - **self.requests_extra_kwargs, - ) - ttwid = spawn.cookies["ttwid"] - - response = [] - first = True - - while len(response) < count: - if count < maxCount: - realCount = count - else: - realCount = maxCount - - query = { - "count": realCount, - "id": 1, - "sourceType": 12, - "itemID": 1, - "insertedItemID": "", - "region": region, - "priority_region": region, - "language": language, - } - api_url = "{}api/recommend/item_list/?{}&{}".format( - BASE_URL, self.__add_url_params__(), urlencode(query) - ) - res = self.get_data(url=api_url, ttwid=ttwid, **kwargs) - for t in res.get("itemList", []): - response.append(t) - - if not res.get("hasMore", False) and not first: - logging.info("TikTok isn't sending more TikToks beyond this point.") - return response[:count] - - realCount = count - len(response) - - first = False - - return response[:count] - - def by_username(self, username, count=30, **kwargs) -> dict: - """Returns a dictionary listing TikToks given a user's username. - - ##### Parameters - * username: The username of the TikTok user to get TikToks from - - * count: The number of posts to return - - Note: seems to only support up to ~2,000 - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id - data = self.get_user_object(username, **kwargs) - return self.user_posts( - data["id"], - data["secUid"], - count=count, - **kwargs, - ) - - def user_liked(self, userID, secUID, count=30, cursor=0, **kwargs) -> dict: - """Returns a dictionary listing TikToks that a given a user has liked. - Note: The user's likes must be public - - ##### Parameters - * userID: The userID of the user, which TikTok assigns - - * secUID: The secUID of the user, which TikTok assigns - - * count: The number of posts to return - - Note: seems to only support up to ~2,000 - * cursor: The offset of a page - - The offset to return new videos from - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id - response = [] - first = True - - while len(response) < count: - if count < maxCount: - realCount = count - else: - realCount = maxCount - - query = { - "count": realCount, - "id": userID, - "type": 2, - "secUid": secUID, - "cursor": cursor, - "sourceType": 9, - "appId": 1233, - "region": region, - "priority_region": region, - "language": language, - } - api_url = "{}api/favorite/item_list/?{}&{}".format( - BASE_URL, self.__add_url_params__(), urlencode(query) - ) - - res = self.get_data(url=api_url, **kwargs) - - try: - res["itemList"] - except Exception: - logging.error("User's likes are most likely private") - return [] - - for t in res.get("itemList", []): - response.append(t) - - if not res.get("hasMore", False) and not first: - logging.info("TikTok isn't sending more TikToks beyond this point.") - return response - - realCount = count - len(response) - cursor = res["cursor"] - - first = False - - return response[:count] - - def user_liked_by_username(self, username, count=30, **kwargs) -> dict: - """Returns a dictionary listing TikToks a user has liked by username. - Note: The user's likes must be public - - ##### Parameters - * username: The username of the user - - * count: The number of posts to return - - Note: seems to only support up to ~2,000 - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id - data = self.get_user_object(username, **kwargs) - return self.user_liked( - data["id"], - data["secUid"], - count=count, - **kwargs, - ) - - def by_sound(self, id, count=30, offset=0, **kwargs) -> dict: - """Returns a dictionary listing TikToks with a specific sound. - - ##### Parameters - * id: The sound id to search by - - Note: Can be found in the URL of the sound specific page or with other methods. - * count: The number of posts to return - - Note: seems to only support up to ~2,000 - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id - response = [] - - while len(response) < count: - if count < maxCount: - realCount = count - else: - realCount = maxCount - - query = { - "secUid": "", - "musicID": str(id), - "count": str(realCount), - "cursor": offset, - "shareUid": "", - "language": language, - } - api_url = "{}api/music/item_list/?{}&{}".format( - BASE_URL, self.__add_url_params__(), urlencode(query) - ) - - res = self.get_data(url=api_url, send_tt_params=True, **kwargs) - - try: - for t in res["items"]: - response.append(t) - except KeyError: - for t in res.get("itemList", []): - response.append(t) - - if not res.get("hasMore", False): - logging.info("TikTok isn't sending more TikToks beyond this point.") - return response - - realCount = count - len(response) - offset = res["cursor"] - - return response[:count] - - def by_sound_page(self, id, page_size=30, cursor=0, **kwargs) -> dict: - """Returns a page of tiktoks with a specific sound. - - Parameters - ---------- - id: The sound id to search by - Note: Can be found in the URL of the sound specific page or with other methods. - cursor: offset for pagination - page_size: The number of posts to return - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id - - query = { - "musicID": str(id), - "count": str(page_size), - "cursor": cursor, - "language": language, - } - api_url = "{}api/music/item_list/?{}&{}".format( - BASE_URL, self.__add_url_params__(), urlencode(query) - ) - - return self.get_data(url=api_url, send_tt_params=True, **kwargs) - - def get_music_object(self, id, **kwargs) -> dict: - """Returns a music object for a specific sound id. - - ##### Parameters - * id: The sound id to get the object for - - This can be found by using other methods. - """ - return self.get_music_object_full(id, **kwargs)["music"] - - def get_music_object_full(self, id, **kwargs): - """Returns a music object for a specific sound id. - - ##### Parameters - * id: The sound id to get the object for - - This can be found by using other methods. - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - r = requests.get( - "https://www.tiktok.com/music/-{}".format(id), - headers={ - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", - "Accept-Encoding": "gzip, deflate", - "Connection": "keep-alive", - "User-Agent": self.userAgent, - }, - proxies=self._format_proxy(kwargs.get("proxy", None)), - cookies=self.get_cookies(**kwargs), - **self.requests_extra_kwargs, - ) - - j_raw = self.__extract_tag_contents(r.text) - return json.loads(j_raw)["props"]["pageProps"]["musicInfo"] - - def get_music_object_full_by_api(self, id, **kwargs): - """Returns a music object for a specific sound id, but using the API rather than HTML requests. - - ##### Parameters - * id: The sound id to get the object for - - This can be found by using other methods. - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id - - api_url = "{}node/share/music/-{}?{}".format( - BASE_URL, id, self.__add_url_params__() - ) - res = self.get_data(url=api_url, **kwargs) - - if res.get("statusCode", 200) == 10203: - raise TikTokNotFoundError() - - return res["musicInfo"] - - def by_hashtag(self, hashtag, count=30, offset=0, **kwargs) -> dict: - """Returns a dictionary listing TikToks with a specific hashtag. - - ##### Parameters - * hashtag: The hashtag to search by - - Without the # symbol - - A valid string is "funny" - * count: The number of posts to return - Note: seems to only support up to ~2,000 - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id - id = self.get_hashtag_object(hashtag)["challengeInfo"]["challenge"]["id"] - response = [] - - required_count = count - - while len(response) < required_count: - if count > maxCount: - count = maxCount - query = { - "count": count, - "challengeID": id, - "type": 3, - "secUid": "", - "cursor": offset, - "priority_region": "", - } - api_url = "{}api/challenge/item_list/?{}&{}".format( - BASE_URL, self.__add_url_params__(), urlencode(query) - ) - res = self.get_data(url=api_url, **kwargs) - - for t in res.get("itemList", []): - response.append(t) - - if not res.get("hasMore", False): - logging.info("TikTok isn't sending more TikToks beyond this point.") - return response - - offset += maxCount - - return response[:required_count] - - def get_hashtag_object(self, hashtag, **kwargs) -> dict: - """Returns a hashtag object. - - ##### Parameters - * hashtag: The hashtag to search by - - Without the # symbol - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id - query = {"name": hashtag, "isName": True, "lang": language} - api_url = "{}node/share/tag/{}?{}&{}".format( - BASE_URL, quote(hashtag), self.__add_url_params__(), urlencode(query) - ) - data = self.get_data(url=api_url, **kwargs) - if data["challengeInfo"].get("challenge") is None: - raise TikTokNotFoundError("Challenge {} does not exist".format(hashtag)) - return data - - def get_recommended_tiktoks_by_video_id(self, id, count=30, **kwargs) -> dict: - """Returns a dictionary listing reccomended TikToks for a specific TikTok video. - - - ##### Parameters - * id: The id of the video to get suggestions for - - Can be found using other methods - * count: The count of results you want to return - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id - - response = [] - first = True - - while len(response) < count: - if count < maxCount: - realCount = count - else: - realCount = maxCount - - query = { - "count": realCount, - "id": 1, - "secUid": "", - "sourceType": 12, - "appId": 1233, - "region": region, - "priority_region": region, - "language": language, - } - api_url = "{}api/recommend/item_list/?{}&{}".format( - BASE_URL, self.__add_url_params__(), urlencode(query) - ) - - res = self.get_data(url=api_url, **kwargs) - - for t in res.get("itemList", []): - response.append(t) - - if not res.get("hasMore", False) and not first: - logging.info("TikTok isn't sending more TikToks beyond this point.") - return response[:count] - - realCount = count - len(response) - - first = False - - return response[:count] - - def get_tiktok_by_id(self, id, **kwargs) -> dict: - """Returns a dictionary of a specific TikTok. - - ##### Parameters - * id: The id of the TikTok you want to get the object for - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id - device_id = kwargs.get("custom_device_id", None) - query = { - "itemId": id, - "language": language, - } - api_url = "{}api/item/detail/?{}&{}".format( - BASE_URL, self.__add_url_params__(), urlencode(query) - ) - - return self.get_data(url=api_url, **kwargs) - - def get_tiktok_by_url(self, url, **kwargs) -> dict: - """Returns a dictionary of a TikTok object by url. - - - ##### Parameters - * url: The TikTok url you want to retrieve - - """ - - url = requests.head(url=url, allow_redirects=True).url - - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id - custom_device_id = kwargs.get("custom_device_id", None) - if "@" in url and "/video/" in url: - post_id = url.split("/video/")[1].split("?")[0] - else: - raise Exception( - "URL format not supported. Below is an example of a supported url.\n" - "https://www.tiktok.com/@therock/video/6829267836783971589" - ) - - return self.get_tiktok_by_id( - post_id, - **kwargs, - ) - - def get_tiktok_by_html(self, url, **kwargs) -> dict: - """This method retrieves a TikTok using the html - endpoints rather than the API based ones. - - ##### Parameters - * url: The url of the TikTok to get - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - - r = requests.get( - url, - headers={ - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", - "path": url.split("tiktok.com")[1], - "Accept-Encoding": "gzip, deflate", - "Connection": "keep-alive", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36", - }, - proxies=self._format_proxy(kwargs.get("proxy", None)), - cookies=self.get_cookies(**kwargs), - **self.requests_extra_kwargs, - ) - - t = r.text - try: - j_raw = self.__extract_tag_contents(r.text) - except IndexError: - if not t: - logging.error("TikTok response is empty") - else: - logging.error("TikTok response: \n " + t) - raise TikTokCaptchaError() - - data = json.loads(j_raw)["props"]["pageProps"] - - if data["serverCode"] == 404: - raise TikTokNotFoundError("TikTok with that url doesn't exist") - - return data - - def get_user_object(self, username, **kwargs) -> dict: - """Gets a user object (dictionary) - - ##### Parameters - * username: The username of the user - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id - return self.get_user(username, **kwargs)["userInfo"]["user"] - - - - def get_video_by_tiktok(self, data, **kwargs) -> bytes: - """Downloads video from TikTok using a TikTok object. - - You will need to set a custom_device_id to do this for anything but trending. - To do this, this is pretty simple you can either generate one yourself or, - you can pass the generate_static_device_id=True into the constructor of the - TikTokApi class. - - ##### Parameters - * data: A TikTok object - - A TikTok JSON object from any other method. - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id - try: - api_url = data["video"]["downloadAddr"] - except Exception: - try: - api_url = data["itemInfos"]["video"]["urls"][0] - except Exception: - api_url = data["itemInfo"]["itemStruct"]["video"]["playAddr"] - return self.get_video_by_download_url(api_url, **kwargs) - - def get_video_by_download_url(self, download_url, **kwargs) -> bytes: - """Downloads video from TikTok using download url in a TikTok object - - ##### Parameters - * download_url: The download url key value in a TikTok object - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id - return self.get_bytes(url=download_url, **kwargs) - - def get_video_by_url(self, video_url, **kwargs) -> bytes: - """Downloads a TikTok video by a URL - - ##### Parameters - * video_url: The TikTok url to download the video from - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id - - tiktok_schema = self.get_tiktok_by_url(video_url, **kwargs) - download_url = tiktok_schema["itemInfo"]["itemStruct"]["video"]["downloadAddr"] - - return self.get_bytes(url=download_url, **kwargs) - - def get_video_no_watermark(self, video_url, return_bytes=1, **kwargs) -> bytes: - """Gets the video with no watermark - .. deprecated:: - - Deprecated due to TikTok fixing this - - ##### Parameters - * video_url: The url of the video you want to download - - * return_bytes: Set this to 0 if you want url, 1 if you want bytes - """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - raise Exception("Deprecated method, TikTok fixed this.") - - def get_music_title(self, id, **kwargs): - """Retrieves a music title given an ID - - ##### Parameters - * id: The music id to get the title for - """ - r = requests.get( - "https://www.tiktok.com/music/-{}".format(id), - headers={ - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", - "Accept-Encoding": "gzip, deflate", - "Connection": "keep-alive", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36", - }, - proxies=self._format_proxy(kwargs.get("proxy", None)), - cookies=self.get_cookies(**kwargs), - **self.requests_extra_kwargs, - ) - t = r.text - j_raw = self.__extract_tag_contents(r.text) - - music_object = json.loads(j_raw)["props"]["pageProps"]["musicInfo"] - if not music_object.get("title", None): - raise TikTokNotFoundError("Song of {} id does not exist".format(str(id))) - - return music_object["title"] - - def get_secuid(self, username, **kwargs): - """Gets the secUid for a specific username - - ##### Parameters - * username: The username to get the secUid for - """ - r = requests.get( - "https://tiktok.com/@{}?lang=en".format(quote(username)), - headers={ - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", - "path": "/@{}".format(quote(username)), - "Accept-Encoding": "gzip, deflate", - "Connection": "keep-alive", - "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36", - }, - proxies=self._format_proxy( - kwargs.get("proxy", None), cookies=self.get_cookies(**kwargs) - ), - **self.requests_extra_kwargs, - ) - try: - return r.text.split('"secUid":"')[1].split('","secret":')[0] - except IndexError as e: - logging.info(r.text) - logging.error(e) - raise Exception( - "Retrieving the user secUid failed. Likely due to TikTok wanting captcha validation. Try to use a proxy." - ) - @staticmethod def generate_device_id(): """Generates a valid device_id for other methods. Pass this as the custom_device_id field to download videos""" @@ -1258,7 +528,7 @@ def __get_js(self, proxy=None) -> str: **self.requests_extra_kwargs, ).text - def __format_new_params__(self, parm) -> str: + def __format_new_params(self, parm) -> str: # TODO: Maybe try not doing this? It should be handled by the urlencode. return parm.replace("/", "%2F").replace(" ", "+").replace(";", "%3B") @@ -1278,7 +548,7 @@ def _add_url_params(self) -> str: "browser_language": self.browser_language.lower() or "en-us", "browser_platform": "iPhone", "browser_name": "Mozilla", - "browser_version": self.__format_new_params__(self.userAgent), + "browser_version": self.__format_new_params(self.user_agent), "browser_online": "true", "timezone_name": self.timezone_name or "America/Chicago", "is_page_visible": "true", diff --git a/docs/TikTokApi/tiktok.html b/docs/TikTokApi/tiktok.html index 1bdf6282..a6f8e09f 100644 --- a/docs/TikTokApi/tiktok.html +++ b/docs/TikTokApi/tiktok.html @@ -235,8 +235,8 @@

self.userAgent = self.browser.userAgent try: - self.timezone_name = self.__format_new_params__(self.browser.timezone_name) - self.browser_language = self.__format_new_params__( + self.timezone_name = self.___format_new_params__(self.browser.timezone_name) + self.browser_language = self.___format_new_params__( self.browser.browser_language ) self.width = self.browser.width @@ -1682,7 +1682,7 @@

**self.requests_extra_kwargs, ).text - def __format_new_params__(self, parm) -> str: + def ___format_new_params__(self, parm) -> str: # TODO: Maybe try not doing this? It should be handled by the urlencode. return parm.replace("/", "%2F").replace(" ", "+").replace(";", "%3B") @@ -1702,7 +1702,7 @@

"browser_language": self.browser_language.lower() or "en-us", "browser_platform": "iPhone", "browser_name": "Mozilla", - "browser_version": self.__format_new_params__(self.userAgent), + "browser_version": self.___format_new_params__(self.userAgent), "browser_online": "true", "timezone_name": self.timezone_name or "America/Chicago", "is_page_visible": "true", @@ -1802,8 +1802,8 @@

self.userAgent = self.browser.userAgent try: - self.timezone_name = self.__format_new_params__(self.browser.timezone_name) - self.browser_language = self.__format_new_params__( + self.timezone_name = self.___format_new_params__(self.browser.timezone_name) + self.browser_language = self.___format_new_params__( self.browser.browser_language ) self.width = self.browser.width @@ -3249,7 +3249,7 @@

**self.requests_extra_kwargs, ).text - def __format_new_params__(self, parm) -> str: + def ___format_new_params__(self, parm) -> str: # TODO: Maybe try not doing this? It should be handled by the urlencode. return parm.replace("/", "%2F").replace(" ", "+").replace(";", "%3B") @@ -3269,7 +3269,7 @@

"browser_language": self.browser_language.lower() or "en-us", "browser_platform": "iPhone", "browser_name": "Mozilla", - "browser_version": self.__format_new_params__(self.userAgent), + "browser_version": self.___format_new_params__(self.userAgent), "browser_online": "true", "timezone_name": self.timezone_name or "America/Chicago", "is_page_visible": "true", @@ -3365,8 +3365,8 @@

self.userAgent = self.browser.userAgent try: - self.timezone_name = self.__format_new_params__(self.browser.timezone_name) - self.browser_language = self.__format_new_params__( + self.timezone_name = self.___format_new_params__(self.browser.timezone_name) + self.browser_language = self.___format_new_params__( self.browser.browser_language ) self.width = self.browser.width From 7905b35f9195731629b144a59563d7c21a21a6aa Mon Sep 17 00:00:00 2001 From: davidteather Date: Sun, 23 Jan 2022 13:53:23 -0600 Subject: [PATCH 03/17] Finish higher level modeling of objects --- README.md | 4 +- TikTokApi/__init__.py | 1 - TikTokApi/api/hashtag.py | 58 +- TikTokApi/api/search.py | 128 +- TikTokApi/api/{music.py => sound.py} | 70 +- TikTokApi/api/trending.py | 16 +- TikTokApi/api/user.py | 160 +- TikTokApi/api/video.py | 81 +- TikTokApi/browser_utilities/browser.py | 2 +- .../browser_utilities/browser_selenium.py | 2 +- TikTokApi/exceptions.py | 2 +- TikTokApi/helpers.py | 17 +- TikTokApi/tiktok.py | 72 +- TikTokApi/tiktokuser.py | 101 - docs/TikTokApi.html | 400 -- docs/TikTokApi/browser_utilities.html | 253 - docs/TikTokApi/browser_utilities/browser.html | 1187 ---- .../browser_utilities/browser_interface.html | 384 -- .../browser_utilities/browser_selenium.html | 1037 --- .../browser_utilities/get_acrawler.html | 298 - docs/TikTokApi/browser_utilities/stealth.html | 1467 ---- docs/TikTokApi/exceptions.html | 664 -- docs/TikTokApi/tiktok.html | 5993 ----------------- docs/TikTokApi/tiktokuser.html | 590 -- docs/TikTokApi/utilities.html | 397 -- docs/index.html | 259 - docs/search.json | 1 - tests/test_by_hashtag.py | 26 - tests/test_by_sound.py | 12 - tests/test_by_username.py | 12 - tests/test_get_music_object_full_by_api.py | 12 - tests/test_get_object_routes.py | 30 - tests/test_hashtag.py | 28 + tests/test_integration.py | 25 + tests/test_search.py | 32 + tests/test_search_for.py | 12 - tests/test_sound.py | 22 + tests/test_trending.py | 14 +- tests/test_user.py | 87 +- tests/test_user_pager.py | 55 - tests/test_video.py | 36 + 41 files changed, 616 insertions(+), 13431 deletions(-) rename TikTokApi/api/{music.py => sound.py} (55%) delete mode 100644 TikTokApi/tiktokuser.py delete mode 100644 docs/TikTokApi.html delete mode 100644 docs/TikTokApi/browser_utilities.html delete mode 100644 docs/TikTokApi/browser_utilities/browser.html delete mode 100644 docs/TikTokApi/browser_utilities/browser_interface.html delete mode 100644 docs/TikTokApi/browser_utilities/browser_selenium.html delete mode 100644 docs/TikTokApi/browser_utilities/get_acrawler.html delete mode 100644 docs/TikTokApi/browser_utilities/stealth.html delete mode 100644 docs/TikTokApi/exceptions.html delete mode 100644 docs/TikTokApi/tiktok.html delete mode 100644 docs/TikTokApi/tiktokuser.html delete mode 100644 docs/TikTokApi/utilities.html delete mode 100644 docs/index.html delete mode 100644 docs/search.json delete mode 100644 tests/test_by_hashtag.py delete mode 100644 tests/test_by_sound.py delete mode 100644 tests/test_by_username.py delete mode 100644 tests/test_get_music_object_full_by_api.py delete mode 100644 tests/test_get_object_routes.py create mode 100644 tests/test_hashtag.py create mode 100644 tests/test_integration.py create mode 100644 tests/test_search.py delete mode 100644 tests/test_search_for.py create mode 100644 tests/test_sound.py delete mode 100644 tests/test_user_pager.py create mode 100644 tests/test_video.py diff --git a/README.md b/README.md index c6713c05..d949122a 100644 --- a/README.md +++ b/README.md @@ -72,9 +72,9 @@ from TikTokApi import TikTokApi api = TikTokApi.get_instance() results = 10 -# Since TikTok changed their API you need to use the custom_verifyFp option. +# Since TikTok changed their API you need to use the custom_verify_fp option. # In your web browser you will need to go to TikTok, Log in and get the s_v_web_id value. -trending = api.by_trending(count=results, custom_verifyFp="") +trending = api.by_trending(count=results, custom_verify_fp="") for tiktok in trending: # Prints the id of the tiktok diff --git a/TikTokApi/__init__.py b/TikTokApi/__init__.py index fa9e85e0..af368e66 100644 --- a/TikTokApi/__init__.py +++ b/TikTokApi/__init__.py @@ -4,4 +4,3 @@ __docformat__ = "restructuredtext" from TikTokApi.tiktok import TikTokApi -from TikTokApi.tiktokuser import TikTokUser diff --git a/TikTokApi/api/hashtag.py b/TikTokApi/api/hashtag.py index 1b8b83c1..f4a5047c 100644 --- a/TikTokApi/api/hashtag.py +++ b/TikTokApi/api/hashtag.py @@ -4,21 +4,32 @@ from urllib.parse import urlencode from ..exceptions import * -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Generator, Optional + if TYPE_CHECKING: from ..tiktok import TikTokApi + from .video import Video + -class Hashtag(): +class Hashtag: parent: TikTokApi - def __init__(self, name: Optional[str] = None, id: Optional[str] = None): + def __init__( + self, + name: Optional[str] = None, + id: Optional[str] = None, + data: Optional[str] = None, + ): + self.as_dict = data self.name = name self.id = id + if data is not None: + self.__extract_from_data() - def challenge_info(self, **kwargs) -> dict: - return self.data_full(**kwargs)['challengeInfo']['challenge'] + def info(self, **kwargs) -> dict: + return self.info_full(**kwargs)["challengeInfo"]["challenge"] - def data_full(self, **kwargs) -> dict: + def info_full(self, **kwargs) -> dict: """Returns a hashtag object. ##### Parameters @@ -47,10 +58,10 @@ def data_full(self, **kwargs) -> dict: if data["challengeInfo"].get("challenge") is None: raise TikTokNotFoundError("Challenge {} does not exist".format(self.name)) - + return data - def videos(self, count=30, offset=0, **kwargs) -> dict: + def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]: """Returns a dictionary listing TikToks with a specific hashtag. ##### Parameters @@ -66,30 +77,47 @@ def videos(self, count=30, offset=0, **kwargs) -> dict: ) = self.parent._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id - if not self.id: - self.id = self.challenge_info()['id'] + if self.id is None: + self.id = self.info()["id"] cursor = offset page_size = 30 - while cursor-offset < count: + while cursor - offset < count: query = { "count": page_size, "challengeID": self.id, - "type": 3, - "secUid": "", "cursor": cursor, - "priority_region": "", } path = "api/challenge/item_list/?{}&{}".format( self.parent._add_url_params(), urlencode(query) ) res = self.parent.get_data(path, **kwargs) - for result in res.get("itemList", []): yield result + for result in res.get("itemList", []): + yield self.parent.video(data=result) if not res.get("hasMore", False): logging.info("TikTok isn't sending more TikToks beyond this point.") return cursor = int(res["cursor"]) + + def __extract_from_data(self): + data = self.as_dict + keys = data.keys() + + if "title" in keys: + self.id = data["id"] + self.name = data["title"] + + if None in (self.name, self.id): + logging.error( + f"Failed to create Hashtag with data: {data}\nwhich has keys {data.keys()}" + ) + + def __repr__(self): + return self.__str__() + + def __str__(self): + return f"TikTokApi.hashtag(id='{self.id}', name='{self.name}')" diff --git a/TikTokApi/api/search.py b/TikTokApi/api/search.py index 763c383b..4d1668aa 100644 --- a/TikTokApi/api/search.py +++ b/TikTokApi/api/search.py @@ -2,20 +2,24 @@ from urllib.parse import urlencode -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Generator + +from .user import User +from .sound import Sound +from .hashtag import Hashtag + if TYPE_CHECKING: from ..tiktok import TikTokApi import logging import requests -class Search(): - parent: TikTokApi - def __init__(self, parent: TikTokApi): - Search.parent = parent +class Search: + parent: TikTokApi - def users(self, search_term, count=28, **kwargs) -> list: + @staticmethod + def users(search_term, count=28, **kwargs) -> Generator[User, None, None]: """Returns a list of users that match the search_term ##### Parameters @@ -25,41 +29,94 @@ def users(self, search_term, count=28, **kwargs) -> list: * count: The number of users to return Note: maximum is around 28 for this type of endpoint. """ - return self.discover_type(search_term, prefix="user", count=count, **kwargs) + return Search.discover_type(search_term, prefix="user", count=count, **kwargs) - def music(self, search_term, count=28, **kwargs) -> list: - """Returns a list of music that match the search_term + @staticmethod + def sounds(search_term, count=28, **kwargs) -> Generator[Sound, None, None]: + """Returns a list of sounds that match the search_term ##### Parameters - * search_term: The string to search for music by - This string is the term you want to search for music by. + * search_term: The string to search for sounds by + This string is the term you want to search for sounds by. - * count: The number of music to return + * count: The number of sounds to return Note: maximum is around 28 for this type of endpoint. """ - return self.discover_type(search_term, prefix="music", count=count, **kwargs) + return Search.discover_type(search_term, prefix="music", count=count, **kwargs) - def hashtags(self, search_term, count=28, **kwargs) -> list: + @staticmethod + def hashtags(search_term, count=28, **kwargs) -> Generator[Hashtag, None, None]: """Returns a list of hashtags that match the search_term ##### Parameters - * search_term: The string to search for music by - This string is the term you want to search for music by. + * search_term: The string to search for hashtags by + This string is the term you want to search for hashtags by. - * count: The number of music to return + * count: The number of hashtags to return Note: maximum is around 28 for this type of endpoint. """ - return self.discover_type( + return Search.discover_type( search_term, prefix="challenge", count=count, **kwargs ) - def discover_type(self, search_term, prefix, count=28, offset=0, **kwargs) -> list: + @staticmethod + def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> list: """Returns a list of whatever the prefix type you pass in - ##### Parameters * search_term: The string to search by - * prefix: The prefix of what to search for + * count: The number search results to return + Note: maximum is around 28 for this type of endpoint. + """ + # TODO: Investigate if this is actually working as expected. Doesn't seem to be + ( + region, + language, + proxy, + maxCount, + device_id, + ) = Search.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = device_id + + cursor = offset + page_size = 28 + + while cursor - offset < count: + query = { + "discoverType": 0, + "needItemList": False, + "keyWord": search_term, + "offset": cursor, + "count": page_size, + "useRecommend": False, + "language": "en", + } + path = "api/discover/{}/?{}&{}".format( + prefix, Search.parent._add_url_params(), urlencode(query) + ) + data = Search.parent.get_data(path, **kwargs) + + for x in data.get("userInfoList", []): + yield User(data=x["user"]) + + for x in data.get("musicInfoList", []): + yield Sound(data=x["music"]) + + for x in data.get("challengeInfoList", []): + yield Hashtag(data=x["challenge"]) + + if int(data["offset"]) <= offset: + logging.info("TikTok is not sending videos beyond this point.") + return + + offset = int(data["offset"]) + + @staticmethod + def users_alternate(search_term, count=28, offset=0, **kwargs) -> list: + """Returns a list of whatever the prefix type you pass in + + ##### Parameters + * search_term: The string to search by * count: The number search results to return """ @@ -69,40 +126,39 @@ def discover_type(self, search_term, prefix, count=28, offset=0, **kwargs) -> li proxy, maxCount, device_id, - ) = self.parent._process_kwargs(kwargs) + ) = Search.parent._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id cursor = offset spawn = requests.head( "https://www.tiktok.com", - proxies=self.parent._format_proxy(proxy), - **self.parent.requests_extra_kwargs + proxies=Search.parent._format_proxy(proxy), + **Search.parent.requests_extra_kwargs ) ttwid = spawn.cookies["ttwid"] - while cursor-offset < count: + # For some reason when <= it can be off by one. + while cursor - offset <= count: query = { "keyword": search_term, "cursor": cursor, "app_language": "en", } path = "api/search/{}/full/?{}&{}".format( - prefix, self.parent._add_url_params(), urlencode(query) + "user", Search.parent._add_url_params(), urlencode(query) ) - data = self.parent.get_data(path, use_desktop_base_url=True, ttwid=ttwid, **kwargs) + data = Search.parent.get_data( + path, use_desktop_base_url=True, ttwid=ttwid, **kwargs + ) # When I move to 3.10+ support make this a match switch. - if prefix == 'user': - for result in data.get("user_list", []): yield result - elif prefix == 'music': - for result in data.get("music_list", []): yield result - elif prefix == 'challenge': - for result in data.get("challenge_list", []): yield result - - if data.get('has_more', 0) == 0: + for result in data.get("user_list", []): + yield User(data=result) + + if data.get("has_more", 0) == 0: logging.info("TikTok is not sending videos beyond this point.") return - cursor = int(data.get('cursor')) \ No newline at end of file + cursor = int(data.get("cursor")) diff --git a/TikTokApi/api/music.py b/TikTokApi/api/sound.py similarity index 55% rename from TikTokApi/api/music.py rename to TikTokApi/api/sound.py index c7ef40f4..d0943d1f 100644 --- a/TikTokApi/api/music.py +++ b/TikTokApi/api/sound.py @@ -10,20 +10,33 @@ from ..helpers import extract_tag_contents from ..exceptions import * -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, ClassVar, Generator, Optional + if TYPE_CHECKING: from ..tiktok import TikTokApi + from .user import User + from .video import Video + +class Sound: + parent: ClassVar[TikTokApi] -class Music(): - parent: TikTokApi + id: str + title: Optional[str] + author: Optional[User] - def __init__(self, music_id: str): - self.id = music_id + def __init__(self, id: Optional[str] = None, data: Optional[str] = None): + self.as_dict = data + if data is not None: + self.__extract_from_data() + elif id is None: + raise TypeError("You must provide id parameter.") + else: + self.id = id - def music_info(self, use_html=False, **kwargs) -> dict: + def info(self, use_html=False, **kwargs) -> dict: if use_html: - return self.data_full(**kwargs)['props']['pageProps']['musicInfo'] + return self.info_full(**kwargs)["props"]["pageProps"]["musicInfo"] ( region, @@ -34,17 +47,15 @@ def music_info(self, use_html=False, **kwargs) -> dict: ) = self.parent._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id - path = "node/share/music/-{}?{}".format( - self.id, self.parent._add_url_params() - ) + path = "node/share/music/-{}?{}".format(self.id, self.parent._add_url_params()) res = self.parent.get_data(path, **kwargs) if res.get("statusCode", 200) == 10203: raise TikTokNotFoundError() - return res['musicInfo'] + return res["musicInfo"] - def data_full(self, **kwargs) -> dict: + def info_full(self, **kwargs) -> dict: r = requests.get( "https://www.tiktok.com/music/-{}".format(self.id), headers={ @@ -61,10 +72,10 @@ def data_full(self, **kwargs) -> dict: data = extract_tag_contents(r.text) return json.loads(data) - def videos(self, count=30, offset=0, **kwargs) -> dict: + def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]: """Returns a dictionary listing TikToks with a specific sound. - Note: seems to only support up to ~2,000 + Note: seems to only support up to ~2,000 """ ( region, @@ -78,13 +89,13 @@ def videos(self, count=30, offset=0, **kwargs) -> dict: cursor = offset page_size = 30 - while cursor-offset < count: + while cursor - offset < count: query = { "secUid": "", "musicID": self.id, "cursor": cursor, "shareUid": "", - "count": page_size + "count": page_size, } path = "api/music/item_list/?{}&{}".format( self.parent._add_url_params(), urlencode(query) @@ -92,10 +103,33 @@ def videos(self, count=30, offset=0, **kwargs) -> dict: res = self.parent.get_data(path, send_tt_params=True, **kwargs) - for result in res.get("itemList", []): yield result + for result in res.get("itemList", []): + yield self.parent.video(data=result) if not res.get("hasMore", False): logging.info("TikTok isn't sending more TikToks beyond this point.") return - cursor = int(res["cursor"]) \ No newline at end of file + cursor = int(res["cursor"]) + + def __extract_from_data(self): + data = self.as_dict + keys = data.keys() + + if "authorName" in keys: + self.id = data["id"] + self.title = data["title"] + + if data.get("authorName") is not None: + self.author = self.parent.user(username=data["authorName"]) + + if self.id is None: + logging.error( + f"Failed to create Sound with data: {data}\nwhich has keys {data.keys()}" + ) + + def __repr__(self): + return self.__str__() + + def __str__(self): + return f"TikTokApi.sound(id='{self.id}')" diff --git a/TikTokApi/api/trending.py b/TikTokApi/api/trending.py index 3e622894..aec27468 100644 --- a/TikTokApi/api/trending.py +++ b/TikTokApi/api/trending.py @@ -4,17 +4,19 @@ import requests from urllib.parse import urlencode +from .video import Video + +from typing import TYPE_CHECKING, Generator -from typing import TYPE_CHECKING if TYPE_CHECKING: from ..tiktok import TikTokApi -class Trending(): +class Trending: parent: TikTokApi @staticmethod - def videos(count=30, **kwargs) -> dict: + def videos(count=30, **kwargs) -> Generator[Video, None, None]: """ Gets trending TikToks @@ -24,6 +26,7 @@ def videos(count=30, **kwargs) -> dict: Note: TikTok seems to only support at MOST ~2000 TikToks from a single endpoint. """ + ( region, language, @@ -58,11 +61,12 @@ def videos(count=30, **kwargs) -> dict: Trending.parent._add_url_params(), urlencode(query) ) res = Trending.parent.get_data(path, ttwid=ttwid, **kwargs) - for result in res.get("itemList", []): yield result + for result in res.get("itemList", []): + yield Video(data=result) amount_yielded += len(res.get("itemList", [])) if not res.get("hasMore", False) and not first: logging.info("TikTok isn't sending more TikToks beyond this point.") - return + return - first = False \ No newline at end of file + first = False diff --git a/TikTokApi/api/user.py b/TikTokApi/api/user.py index 3afdbf9a..6717967b 100644 --- a/TikTokApi/api/user.py +++ b/TikTokApi/api/user.py @@ -1,5 +1,4 @@ from __future__ import annotations -from concurrent.futures import process import json import logging @@ -9,29 +8,55 @@ from ..exceptions import * from ..helpers import extract_tag_contents -from .search import Search -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, ClassVar, Generator, Optional + if TYPE_CHECKING: from ..tiktok import TikTokApi - -class User(): - parent: TikTokApi - - def __init__(self, username: Optional[str] = None, user_id: Optional[str] = None, sec_uid: Optional[str] = None): - self.username = username - self.user_id = user_id - self.sec_uid = sec_uid - - def user_object(self, **kwargs): - return self.data_full(**kwargs)['UserModule']['users'][self.username] - - def data_full(self, **kwargs) -> dict: + from .video import Video + + +class User: + """A TikTok User class + + Attributes + user_id: The TikTok user's ID. + sec_uid: The TikTok user's sec_uid. + username: The TikTok user's username. + as_dict: The dictionary provided to create the class. + """ + + parent: ClassVar[TikTokApi] + + user_id: str + sec_uid: str + username: str + data: dict + + def __init__( + self, + username: Optional[str] = None, + user_id: Optional[str] = None, + sec_uid: Optional[str] = None, + data: Optional[str] = None, + ): + self.__update_id_sec_uid_username(user_id, sec_uid, username) + self.as_dict = data + if data is not None: + self.__extract_from_data() + + def info(self, **kwargs): + # TODO: Might throw a key error with the HTML + return self.info_full(**kwargs)["props"]["pageProps"]["userInfo"]["user"] + + def info_full(self, **kwargs) -> dict: """Gets all data associated with the user.""" # TODO: Find the one using only user_id & sec_uid if not self.username: - raise TypeError('You must provide the username when creating this class to use this method.') + raise TypeError( + "You must provide the username when creating this class to use this method." + ) quoted_username = quote(self.username) r = requests.get( @@ -51,12 +76,14 @@ def data_full(self, **kwargs) -> dict: data = extract_tag_contents(r.text) user = json.loads(data) - if user["UserPage"]["statusCode"] == 404: - raise TikTokNotFoundError("TikTok user with username {} does not exist".format(self.username)) + if user["props"]["pageProps"]["statusCode"] == 404: + raise TikTokNotFoundError( + "TikTok user with username {} does not exist".format(self.username) + ) return user - def videos(self, count=30, cursor=0, **kwargs) -> dict: + def videos(self, count=30, cursor=0, **kwargs) -> Generator[Video, None, None]: """Returns an array of dictionaries representing TikToks for a user. ##### Parameters @@ -80,13 +107,8 @@ def videos(self, count=30, cursor=0, **kwargs) -> dict: amount_yielded = 0 while amount_yielded < count: - if count < maxCount: - realCount = count - else: - realCount = maxCount - query = { - "count": realCount, + "count": 30, "id": self.user_id, "cursor": cursor, "type": 1, @@ -105,17 +127,19 @@ def videos(self, count=30, cursor=0, **kwargs) -> dict: videos = res.get("itemList", []) amount_yielded += len(videos) - for video in videos: yield video + for video in videos: + yield self.parent.video(data=video) if not res.get("hasMore", False) and not first: logging.info("TikTok isn't sending more TikToks beyond this point.") return - realCount = count - amount_yielded cursor = res["cursor"] first = False - def liked(self, count: int = 30, cursor: int = 0, **kwargs): + def liked( + self, count: int = 30, cursor: int = 0, **kwargs + ) -> Generator[Video, None, None]: """Returns a dictionary listing TikToks that a given a user has liked. Note: The user's likes must be public @@ -133,20 +157,18 @@ def liked(self, count: int = 30, cursor: int = 0, **kwargs): proxy, maxCount, device_id, - ) = self._process_kwargs(kwargs) + ) = self.parent._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id amount_yielded = 0 first = True - while amount_yielded < count: - if count < maxCount: - realCount = count - else: - realCount = maxCount + if self.user_id is None and self.sec_uid is None: + self.__find_attributes() + while amount_yielded < count: query = { - "count": realCount, + "count": 30, "id": self.user_id, "type": 2, "secUid": self.sec_uid, @@ -157,11 +179,11 @@ def liked(self, count: int = 30, cursor: int = 0, **kwargs): "priority_region": region, "language": language, } - api_url = "api/favorite/item_list/?{}&{}".format( - User.parent.__add_url_params__(), urlencode(query) + path = "api/favorite/item_list/?{}&{}".format( + User.parent._add_url_params(), urlencode(query) ) - res = self.get_data(url=api_url, **kwargs) + res = self.parent.get_data(path, **kwargs) if "itemList" not in res.keys(): logging.error("User's likes are most likely private") @@ -169,29 +191,59 @@ def liked(self, count: int = 30, cursor: int = 0, **kwargs): videos = res.get("itemList", []) amount_yielded += len(videos) - for video in videos: yield video + for video in videos: + amount_yielded += 1 + yield self.parent.video(data=video) if not res.get("hasMore", False) and not first: logging.info("TikTok isn't sending more TikToks beyond this point.") return - realCount = count - amount_yielded cursor = res["cursor"] first = False - def __find_attributes(self): + def __extract_from_data(self): + data = self.as_dict + keys = data.keys() + + if "user_info" in keys: + self.__update_id_sec_uid_username( + data["user_info"]["uid"], + data["user_info"]["sec_uid"], + data["user_info"]["unique_id"], + ) + elif "uniqueId" in keys: + self.__update_id_sec_uid_username( + data["id"], data["secUid"], data["uniqueId"] + ) + + if None in (self.username, self.user_id, self.sec_uid): + logging.error( + f"Failed to create User with data: {data}\nwhich has keys {data.keys()}" + ) + + def __update_id_sec_uid_username(self, id, sec_uid, username): + self.user_id = id + self.sec_uid = sec_uid + self.username = username + + def __find_attributes(self) -> None: # It is more efficient to check search first, since self.user_object() makes HTML request. - user_object = None - for r in Search(self.parent).users(self.username): - if r['user_info']['unique_id'] == self.username: - user_object = r['user_info'] - self.user_id = user_object['uid'] - self.sec_uid = user_object['sec_uid'] - self.username = user_object['unique_id'] + found = False + for u in self.parent.search.users(self.username): + if u.username == self.username: + found = True + self.__update_id_sec_uid_username(u.user_id, u.sec_uid, u.username) break - if user_object is None: - user_object = self.user_object() - self.user_id = user_object['id'] - self.sec_uid = user_object['secUid'] - self.username = user_object['uniqueId'] + if not found: + user_object = self.info() + self.__update_id_sec_uid_username( + user_object["id"], user_object["secUid"], user_object["uniqueId"] + ) + + def __repr__(self): + return self.__str__() + + def __str__(self): + return f"TikTokApi.user(username='{self.username}', user_id='{self.user_id}', sec_uid='{self.sec_uid}')" diff --git a/TikTokApi/api/video.py b/TikTokApi/api/video.py index 2c653791..f11c35e7 100644 --- a/TikTokApi/api/video.py +++ b/TikTokApi/api/video.py @@ -4,25 +4,53 @@ from ..helpers import extract_video_id_from_url -from typing import TYPE_CHECKING, Optional +import logging +from typing import TYPE_CHECKING, ClassVar, Optional + if TYPE_CHECKING: from ..tiktok import TikTokApi + from .user import User + from .sound import Sound + from .hashtag import Hashtag + + +class Video: + """A TikTok Video class -class Video(): - parent: TikTokApi + Attributes + id: The TikTok video ID. + author: The author of the TikTok as a User object. + as_dict: The dictionary provided to create the class. + """ - def __init__(self, id: Optional[str] = None, url: Optional[str] = None, raw: Optional[dict] = None): - if id: - self.id = id - elif url: + parent: ClassVar[TikTokApi] + + id: str + author: Optional[User] + sound: Optional[Sound] + hashtags: Optional[list[Hashtag]] + as_dict: dict + + def __init__( + self, + id: Optional[str] = None, + url: Optional[str] = None, + data: Optional[dict] = None, + ): + self.id = id + self.as_dict = data + if data is not None: + self.__extract_from_data() + elif url is not None: self.id = extract_video_id_from_url(url) - else: + + if self.id is None: raise TypeError("You must provide id or url parameter.") - def video_info(self, **kwargs) -> dict: - return self.data_full(**kwargs)['itemInfo']['itemStruct'] + def info(self, **kwargs) -> dict: + return self.info_full(**kwargs)["itemInfo"]["itemStruct"] - def data_full(self, **kwargs) -> dict: + def info_full(self, **kwargs) -> dict: """Returns a dictionary of a specific TikTok.""" ( region, @@ -53,7 +81,32 @@ def bytes(self, **kwargs) -> bytes: ) = self.parent._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id - video_data = self.video_info(**kwargs) - download_url = video_data['video']['playAddr'] + video_data = self.info(**kwargs) + download_url = video_data["video"]["playAddr"] + + return self.parent.get_bytes(url=download_url, **kwargs) + + def __extract_from_data(self) -> None: + data = self.as_dict + keys = data.keys() + + if "author" in keys: + self.id = data["id"] + self.author = self.parent.user(data=data["author"]) + self.sound = self.parent.sound(data=data["music"]) + + self.hashtags = [ + self.parent.hashtag(data=hashtag) + for hashtag in data.get("challenges", []) + ] + + if self.id is None: + logging.error( + f"Failed to create Video with data: {data}\nwhich has keys {data.keys()}" + ) + + def __repr__(self): + return self.__str__() - return self.parent.get_bytes(url=download_url, **kwargs) \ No newline at end of file + def __str__(self): + return f"TikTokApi.video(id='{self.id}')" diff --git a/TikTokApi/browser_utilities/browser.py b/TikTokApi/browser_utilities/browser.py index c162239c..248982b5 100644 --- a/TikTokApi/browser_utilities/browser.py +++ b/TikTokApi/browser_utilities/browser.py @@ -198,7 +198,7 @@ def process(route): verifyFp = self.gen_verifyFp() else: verifyFp = kwargs.get( - "custom_verifyFp", + "custom_verify_fp", "verify_khgp4f49_V12d4mRX_MdCO_4Wzt_Ar0k_z4RCQC9pUDpX", ) diff --git a/TikTokApi/browser_utilities/browser_selenium.py b/TikTokApi/browser_utilities/browser_selenium.py index e76ec0c4..e8229464 100644 --- a/TikTokApi/browser_utilities/browser_selenium.py +++ b/TikTokApi/browser_utilities/browser_selenium.py @@ -164,7 +164,7 @@ def sign_url(self, calc_tt_params=False, **kwargs): verifyFp = self.gen_verifyFp() else: verifyFp = kwargs.get( - "custom_verifyFp", + "custom_verify_fp", "verify_khgp4f49_V12d4mRX_MdCO_4Wzt_Ar0k_z4RCQC9pUDpX", ) diff --git a/TikTokApi/exceptions.py b/TikTokApi/exceptions.py index 8c7c6460..8f0d79bb 100644 --- a/TikTokApi/exceptions.py +++ b/TikTokApi/exceptions.py @@ -1,7 +1,7 @@ class TikTokCaptchaError(Exception): def __init__( self, - message="TikTok blocks this request displaying a Captcha \nTip: Consider using a proxy or a custom_verifyFp as method parameters", + 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) diff --git a/TikTokApi/helpers.py b/TikTokApi/helpers.py index d835e009..048c40fe 100644 --- a/TikTokApi/helpers.py +++ b/TikTokApi/helpers.py @@ -5,9 +5,13 @@ import re import requests + def extract_tag_contents(html): - next_json = re.search(r'id=\"__NEXT_DATA__\"\s+type=\"application\/json\"\s*[^>]+>\s*(?P[^<]+)', html) - if (next_json): + next_json = re.search( + r"id=\"__NEXT_DATA__\"\s+type=\"application\/json\"\s*[^>]+>\s*(?P[^<]+)", + html, + ) + if next_json: nonce_start = '' nonce = html.split(nonce_start)[1].split(nonce_end)[0] @@ -17,18 +21,21 @@ def extract_tag_contents(html): )[1].split("")[0] return j_raw else: - sigi_json = re.search(r'>\s*window\[[\'"]SIGI_STATE[\'"]\]\s*=\s*(?P{.+});', html) + sigi_json = re.search( + r'>\s*window\[[\'"]SIGI_STATE[\'"]\]\s*=\s*(?P{.+});', html + ) if sigi_json: return sigi_json.group(1) else: raise TikTokCaptchaError() + def extract_video_id_from_url(url): url = requests.head(url=url, allow_redirects=True).url if "@" in url and "/video/" in url: - post_id = url.split("/video/")[1].split("?")[0] + return url.split("/video/")[1].split("?")[0] else: raise TypeError( "URL format not supported. Below is an example of a supported url.\n" "https://www.tiktok.com/@therock/video/6829267836783971589" - ) \ No newline at end of file + ) diff --git a/TikTokApi/tiktok.py b/TikTokApi/tiktok.py index 6c2b09d8..a1efac26 100644 --- a/TikTokApi/tiktok.py +++ b/TikTokApi/tiktok.py @@ -7,7 +7,7 @@ from urllib.parse import quote, urlencode import requests -from TikTokApi.api import user, music, search, hashtag, video, trending +from TikTokApi.api import sound, user, search, hashtag, video, trending from playwright.sync_api import sync_playwright from .exceptions import * @@ -46,7 +46,7 @@ class to prevent issues from arising with playwright Use this if you want to download videos from a script but don't want to generate your own custom_device_id parameter. - * custom_verifyFp: A TikTok parameter needed to work most of the time, optional + * custom_verify_fp: A TikTok parameter needed to work most of the time, optional To get this parameter look at [this video](https://youtu.be/zwLmLfVI-VQ?t=117) I recommend watching the entire thing, as it will help setup this package. All the methods take this as a optional parameter, however it's cleaner code @@ -59,7 +59,7 @@ class to prevent issues from arising with playwright * use_test_endpoints: Send requests to TikTok's test endpoints, optional This parameter when set to true will make requests to TikTok's testing endpoints instead of the live site. I can't guarantee this will work - in the future, however currently basically any custom_verifyFp will + in the future, however currently basically any custom_verify_fp will work here which is helpful. * proxy: A string containing your proxy address, optional @@ -96,17 +96,18 @@ def _initialize(self, **kwargs): user.User.parent = self self.user = user.User - self.search = search.Search(self) + search.Search.parent = self + self.search = search.Search - music.Music.parent = self - self.music = music.Music + sound.Sound.parent = self + self.sound = sound.Sound hashtag.Hashtag.parent = self self.hashtag = hashtag.Hashtag video.Video.parent = self self.video = video.Video - + trending.Trending.parent = self self.trending = trending.Trending @@ -119,13 +120,9 @@ def _initialize(self, **kwargs): if kwargs.get("custom_did") != None: raise Exception("Please use custom_device_id instead of custom_device_id") self.custom_device_id = kwargs.get("custom_device_id", None) - self.user_agent = ( - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " - "AppleWebKit/537.36 (KHTML, like Gecko) " - "Chrome/86.0.4240.111 Safari/537.36" - ) + 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.proxy = kwargs.get("proxy", None) - self.custom_verifyFp = kwargs.get("custom_verifyFp") + self.custom_verify_fp = kwargs.get("custom_verify_fp") self.signer_url = kwargs.get("external_signer", None) self.request_delay = kwargs.get("request_delay", None) self.requests_extra_kwargs = kwargs.get("requests_extra_kwargs", {}) @@ -190,13 +187,13 @@ def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict: if self.proxy is not None: proxy = self.proxy - if kwargs.get("custom_verifyFp") == None: - if self.custom_verifyFp != None: - verifyFp = self.custom_verifyFp + if kwargs.get("custom_verify_fp") == None: + if self.custom_verify_fp != None: + verifyFp = self.custom_verify_fp else: verifyFp = "verify_khr3jabg_V7ucdslq_Vrw9_4KPb_AJ1b_Ks706M8zIJTq" else: - verifyFp = kwargs.get("custom_verifyFp") + verifyFp = kwargs.get("custom_verify_fp") tt_params = None send_tt_params = kwargs.get("send_tt_params", False) @@ -207,17 +204,23 @@ def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict: full_url = BASE_URL + path if self.signer_url is None: - kwargs["custom_verifyFp"] = verifyFp + 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 ) user_agent = self.browser.user_agent referrer = self.browser.referrer else: - verify_fp, device_id, signature, user_agent, referrer = self.external_signer( + ( + verify_fp, + device_id, + signature, + user_agent, + referrer, + ) = self.external_signer( full_url, custom_device_id=kwargs.get("custom_device_id"), - verifyFp=kwargs.get("custom_verifyFp", verifyFp), + verifyFp=kwargs.get("custom_verify_fp", verifyFp), ) if not kwargs.get("send_tt_params", False): @@ -276,7 +279,9 @@ def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict: logging.error( "Tiktok wants to display a catcha. Response is:\n" + r.text ) - logging.error(self.get_cookies(**kwargs)) + logging.info( + f"Request failed with\nurl:{url}\nheaders:{headers}\ncookies:{self.get_cookies(**kwargs)}" + ) raise TikTokCaptchaError() # statusCode from props->pageProps->statusCode thanks @adiantek on #403 @@ -379,7 +384,7 @@ def external_signer(self, url, custom_device_id=None, verifyFp=None): for some things such as video download you will need to set a consistent one of these. - * custom_verifyFp: A TikTok parameter needed to work most of the time, + * custom_verify_fp: A TikTok parameter needed to work most of the time, To get this parameter look at [this video](https://youtu.be/zwLmLfVI-VQ?t=117) I recommend watching the entire thing, as it will help setup this package. """ @@ -411,13 +416,13 @@ def get_cookies(self, **kwargs): "custom_device_id", "".join(random.choice(string.digits) for num in range(19)), ) - if kwargs.get("custom_verifyFp") == None: - if self.custom_verifyFp != None: - verifyFp = self.custom_verifyFp + if kwargs.get("custom_verify_fp") is None: + if self.custom_verify_fp is not None: + verifyFp = self.custom_verify_fp else: - verifyFp = "verify_khr3jabg_V7ucdslq_Vrw9_4KPb_AJ1b_Ks706M8zIJTq" + verifyFp = None else: - verifyFp = kwargs.get("custom_verifyFp") + verifyFp = kwargs.get("custom_verify_fp") if kwargs.get("force_verify_fp_on_cookie_header", False): return { @@ -460,7 +465,13 @@ def get_bytes(self, **kwargs) -> bytes: user_agent = self.browser.user_agent referrer = self.browser.referrer else: - verify_fp, device_id, signature, user_agent, referrer = self.external_signer( + ( + verify_fp, + device_id, + signature, + user_agent, + referrer, + ) = self.external_signer( kwargs["url"], custom_device_id=kwargs.get("custom_device_id", None) ) query = {"verifyFp": verify_fp, "_signature": signature} @@ -520,7 +531,6 @@ def _process_kwargs(self, kwargs): device_id = "".join(random.choice(string.digits) for num in range(19)) return region, language, proxy, maxCount, device_id - def __get_js(self, proxy=None) -> str: return requests.get( "https://sf16-muse-va.ibytedtos.com/obj/rc-web-sdk-gcs/acrawler.js", @@ -541,14 +551,13 @@ def _add_url_params(self) -> str: "priority_region": "", "os": "ios", "referer": "", - "root_referer": "", "cookie_enabled": "true", "screen_width": self.width, "screen_height": self.height, "browser_language": self.browser_language.lower() or "en-us", "browser_platform": "iPhone", "browser_name": "Mozilla", - "browser_version": self.__format_new_params(self.user_agent), + "browser_version": self.user_agent, "browser_online": "true", "timezone_name": self.timezone_name or "America/Chicago", "is_page_visible": "true", @@ -557,4 +566,5 @@ def _add_url_params(self) -> str: "history_len": random.randint(0, 30), "language": self.language or "en", } + return urlencode(query) diff --git a/TikTokApi/tiktokuser.py b/TikTokApi/tiktokuser.py deleted file mode 100644 index 87be56be..00000000 --- a/TikTokApi/tiktokuser.py +++ /dev/null @@ -1,101 +0,0 @@ -import requests - - -class TikTokUser: - def __init__(self, user_cookie, debug=False, proxy=None): - """A TikTok User Class. Represents a single user that is logged in. - - :param user_cookie: The cookies from a signed in session of TikTok. - Sign into TikTok.com and run document.cookie in the javascript console - and then copy the string and place it into this parameter. - """ - self.cookies = user_cookie - self.debug = debug - self.proxy = proxy - - def get_insights(self, videoID, username=None, proxy=None) -> dict: - """Get insights/analytics for a video. - - :param videoID: The TikTok ID to look up the insights for. - """ - api_url = "https://api.tiktok.com/aweme/v1/data/insighs/?tz_offset=-25200&aid=1233&carrier_region=US" - if username is not None: - referrer = "https://www.tiktok.com/@" + username + "/video/" + videoID - else: - referrer = "https://www.tiktok.com/" - insights = [ - "video_info", - "video_page_percent", - "video_region_percent", - "video_total_duration", - "video_per_duration", - ] - # Note: this list of parameters has to be in exactly this order with exactly this format - # or else you will get "Invalid parameters" - - def build_insight(insight, videoID): - return '{"insigh_type":"' + insight + '","aweme_id":"' + videoID + '"}' - - insight_string = ",".join([build_insight(i, videoID) for i in insights]) - insight_string = ( - insight_string - + ',{"insigh_type": "user_info"}' - + ',{"insigh_type":"video_uv","aweme_id":"' - + videoID - + '"}' - + ',{"insigh_type":"vv_history","days":8}' - + ',{"insigh_type":"follower_num_history","days":9}' - + ',{"insigh_type":"follower_num"}' - + ',{"insigh_type":"user_info"}' - ) - r = requests.post( - api_url, - headers={ - "accept": "*/*", - "accept-language": "en-US,en;q=0.9", - "content-type": "application/x-www-form-urlencoded", - "sec-fetch-dest": "empty", - "sec-fetch-mode": "cors", - "sec-fetch-site": "same-site", - "referrer": referrer, - "referrerPolicy": "no-referrer-when-downgrade", - "method": "POST", - "mode": "cors", - "credentials": "include", - }, - data="type_requests=[" + insight_string + "]", - proxies=self.__format_proxy(proxy), - cookies=self.__cookies_to_json(self.cookies), - ) - try: - return r.json() - except Exception: - if debug: - print(f"Failed converting following to JSON\n{r.text}") - raise Exception("Invalid Response (from TikTok)") - - # - # PRIVATE METHODS - # - def __format_proxy(self, proxy) -> dict: - """ - Formats the proxy object - """ - if proxy is not None: - return {"http": proxy, "https": proxy} - else: - return None - - def __cookies_to_json(self, cookie_string) -> dict: - """ - Turns a cookie string into a dict for - use in the requests module - """ - if isinstance(cookie_string, dict): - return cookie_string - - cookie_dict = {} - for cookie in cookie_string.split("; "): - cookie_dict[cookie.split("=")[0]] = cookie.split("=")[1] - - return cookie_dict diff --git a/docs/TikTokApi.html b/docs/TikTokApi.html deleted file mode 100644 index c7ca002e..00000000 --- a/docs/TikTokApi.html +++ /dev/null @@ -1,400 +0,0 @@ - - - - - - - TikTokApi API documentation - - - - - - - - -
-
-

-TikTokApi

- -

Unofficial TikTok API in Python

- -

This is an unofficial api wrapper for TikTok.com in python. With this api you are able to call most trending and fetch specific user information as well as much more.

- -

DOI LinkedIn Sponsor Me GitHub release (latest by date) Build Status GitHub Downloads Support Server

- -

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.

- -

TikAPI | 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

- - - -

Getting Started

- -

To get started using this api follow the instructions below.

- -

How to support the project

- -
    -
  • Feel free to sponsor me on GitHub
  • -
  • Feel free to tip the project using the brave browser
  • -
  • Submit PRs for issues :)
  • -
- -

Installing

- -

If you run into an issue please check the closed issues on the github. You're most likely not the first person to experience this issue. If nothing works feel free to open an issue.

- -
pip install TikTokApi
-python -m playwright install
-
- -

If you would prefer a video walk through of setting up this package I created a YouTube video just for that.

- -

If you're on MacOS you may need to install XCode Developer Tools

- -

Docker Installation

- -

Clone this repository onto a local machine then run the following commands.

- -
docker pull mcr.microsoft.com/playwright:focal
-docker build . -t tiktokapi:latest
-docker run -v TikTokApi --rm tiktokapi:latest python3 your_script.py
-
- -

Note this assumes your script is named your_script.py and lives in the root of this directory.

- -

Common Issues

- -

Please don't open an issue if you're experiencing one of these just comment if the provided solution do not work for you.

- -
    -
  • Browser Has no Attribute - make sure you ran python3 -m playwright install, if your error persists try the playwright quickstart guide and diagnose issues from there.
  • -
- -

Quick Start Guide

- -

Here's a quick bit of code to get the most recent trending on TikTok. There's more examples in the examples directory.

- -
from TikTokApi import TikTokApi
-api = TikTokApi.get_instance()
-results = 10
-
-# Since TikTok changed their API you need to use the custom_verifyFp option. 
-# In your web browser you will need to go to TikTok, Log in and get the s_v_web_id value.
-trending = api.by_trending(count=results, custom_verifyFp="")
-
-for tiktok in trending:
-    # Prints the id of the tiktok
-    print(tiktok['id'])
-
-print(len(trending))
-
- -

To run the example scripts from the repository root, make sure you use the -module form of python the interpreter

- -
python -m examples.get_trending
-
- -

Here's an example of what a TikTok dictionary looks like.

- -

Documentation

- -

You can find the documentation here (you'll likely just need the TikTokApi section of the docs), I will be making this documentation more complete overtime as it's not super great right now, but better than just having it in the readme!

- -

Authors

- - - -

See also the list of contributors who participated in this project.

- -

License

- -

This project is licensed under the MIT License

-
- -
- View Source -
"""
-.. include:: ../README.md
-"""
-__docformat__ = "restructuredtext"
-
-from TikTokApi.tiktok import TikTokApi
-from TikTokApi.tiktokuser import TikTokUser
-
- -
- -
-
- - - - \ No newline at end of file diff --git a/docs/TikTokApi/browser_utilities.html b/docs/TikTokApi/browser_utilities.html deleted file mode 100644 index 6b7c5e06..00000000 --- a/docs/TikTokApi/browser_utilities.html +++ /dev/null @@ -1,253 +0,0 @@ - - - - - - - TikTokApi.browser_utilities API documentation - - - - - - - - -
-
-

-TikTokApi.browser_utilities

- - -
- View Source -
from .browser_interface import BrowserInterface
-
- -
- -
-
- - - - \ No newline at end of file diff --git a/docs/TikTokApi/browser_utilities/browser.html b/docs/TikTokApi/browser_utilities/browser.html deleted file mode 100644 index 70c096c6..00000000 --- a/docs/TikTokApi/browser_utilities/browser.html +++ /dev/null @@ -1,1187 +0,0 @@ - - - - - - - TikTokApi.browser_utilities.browser API documentation - - - - - - - - -
-
-

-TikTokApi.browser_utilities.browser

- - -
- View Source -
import random
-import time
-import string
-import requests
-import logging
-import time
-import random
-import json
-import re
-from .browser_interface import BrowserInterface
-from urllib.parse import splitquery, parse_qs, parse_qsl
-
-
-# Import Detection From Stealth
-from .get_acrawler import get_acrawler, get_tt_params_script
-from playwright.sync_api import sync_playwright
-
-playwright = None
-
-
-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,
-        **kwargs,
-    ):
-        self.kwargs = kwargs
-        self.debug = kwargs.get("debug", False)
-        self.proxy = kwargs.get("proxy", None)
-        self.api_url = kwargs.get("api_url", None)
-        self.referrer = kwargs.get("referer", "https://www.tiktok.com/")
-        self.language = kwargs.get("language", "en")
-        self.executablePath = kwargs.get("executablePath", None)
-        self.device_id = kwargs.get("custom_device_id", None)
-
-        args = kwargs.get("browser_args", [])
-        options = kwargs.get("browser_options", {})
-
-        if len(args) == 0:
-            self.args = []
-        else:
-            self.args = args
-
-        self.options = {
-            "headless": True,
-            "handle_sigint": True,
-            "handle_sigterm": True,
-            "handle_sighup": True,
-        }
-
-        if self.proxy is not None:
-            if "@" in self.proxy:
-                server_prefix = self.proxy.split("://")[0]
-                address = self.proxy.split("@")[1]
-                self.options["proxy"] = {
-                    "server": server_prefix + "://" + address,
-                    "username": self.proxy.split("://")[1].split(":")[0],
-                    "password": self.proxy.split("://")[1].split("@")[0].split(":")[1],
-                }
-            else:
-                self.options["proxy"] = {"server": self.proxy}
-
-        self.options.update(options)
-
-        if self.executablePath is not None:
-            self.options["executablePath"] = self.executablePath
-
-        try:
-            self.browser = get_playwright().webkit.launch(
-                args=self.args, **self.options
-            )
-        except Exception as e:
-            logging.critical(e)
-            raise e
-
-        context = self.create_context(set_useragent=True)
-        page = context.new_page()
-        self.get_params(page)
-        context.close()
-
-    def get_params(self, page) -> None:
-        self.browser_language = self.kwargs.get(
-            "browser_language",
-            page.evaluate("""() => { return navigator.language; }"""),
-        )
-        self.browser_version = page.evaluate(
-            """() => { return window.navigator.appVersion; }"""
-        )
-
-        if len(self.browser_language.split("-")) == 0:
-            self.region = self.kwargs.get("region", "US")
-            self.language = self.kwargs.get("language", "en")
-        elif len(self.browser_language.split("-")) == 1:
-            self.region = self.kwargs.get("region", "US")
-            self.language = self.browser_language.split("-")[0]
-        else:
-            self.region = self.kwargs.get("region", self.browser_language.split("-")[1])
-            self.language = self.kwargs.get(
-                "language", self.browser_language.split("-")[0]
-            )
-
-        self.timezone_name = self.kwargs.get(
-            "timezone_name",
-            page.evaluate(
-                """() => { return Intl.DateTimeFormat().resolvedOptions().timeZone; }"""
-            ),
-        )
-        self.width = page.evaluate("""() => { return screen.width; }""")
-        self.height = page.evaluate("""() => { return screen.height; }""")
-
-    def create_context(self, set_useragent=False):
-        iphone = playwright.devices["iPhone 11 Pro"]
-        iphone["viewport"] = {
-            "width": random.randint(320, 1920),
-            "height": random.randint(320, 1920),
-        }
-        iphone["device_scale_factor"] = random.randint(1, 3)
-        iphone["is_mobile"] = random.randint(1, 2) == 1
-        iphone["has_touch"] = random.randint(1, 2) == 1
-
-        iphone["bypass_csp"] = True
-
-        context = self.browser.new_context(**iphone)
-        if set_useragent:
-            self.userAgent = iphone["user_agent"]
-
-        return context
-
-    def base36encode(self, number, alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
-        """Converts an integer to a base36 string."""
-        base36 = ""
-        sign = ""
-
-        if number < 0:
-            sign = "-"
-            number = -number
-
-        if 0 <= number < len(alphabet):
-            return sign + alphabet[number]
-
-        while number != 0:
-            number, i = divmod(number, len(alphabet))
-            base36 = alphabet[i] + base36
-
-        return sign + base36
-
-    def gen_verifyFp(self):
-        chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[:]
-        chars_len = len(chars)
-        scenario_title = self.base36encode(int(time.time() * 1000))
-        uuid = [0] * 36
-        uuid[8] = "_"
-        uuid[13] = "_"
-        uuid[18] = "_"
-        uuid[23] = "_"
-        uuid[14] = "4"
-
-        for i in range(36):
-            if uuid[i] != 0:
-                continue
-            r = int(random.random() * chars_len)
-            uuid[i] = chars[int((3 & r) | 8 if i == 19 else r)]
-
-        return f'verify_{scenario_title.lower()}_{"".join(uuid)}'
-
-    def sign_url(self, calc_tt_params=False, **kwargs):
-        def process(route):
-            route.abort()
-
-        url = kwargs.get("url", None)
-        if url is None:
-            raise Exception("sign_url required a url parameter")
-
-        tt_params = None
-        context = self.create_context()
-        page = context.new_page()
-
-        if calc_tt_params:
-            page.route(re.compile(r"(\.png)|(\.jpeg)|(\.mp4)|(x-expire)"), process)
-            page.goto(
-                kwargs.get("default_url", "https://www.tiktok.com/@redbull"),
-                wait_until="load",
-            )
-
-        verifyFp = "".join(
-            random.choice(
-                string.ascii_lowercase + string.ascii_uppercase + string.digits
-            )
-            for i in range(16)
-        )
-        if kwargs.get("gen_new_verifyFp", False):
-            verifyFp = self.gen_verifyFp()
-        else:
-            verifyFp = kwargs.get(
-                "custom_verifyFp",
-                "verify_khgp4f49_V12d4mRX_MdCO_4Wzt_Ar0k_z4RCQC9pUDpX",
-            )
-
-        if kwargs.get("custom_device_id") is not None:
-            device_id = kwargs.get("custom_device_id", None)
-        elif self.device_id is None:
-            device_id = str(random.randint(10000, 999999999))
-        else:
-            device_id = self.device_id
-
-        url = "{}&verifyFp={}&device_id={}".format(url, verifyFp, device_id)
-
-        page.add_script_tag(content=get_acrawler())
-        evaluatedPage = page.evaluate(
-            '''() => {
-            var url = "'''
-            + url
-            + """"
-            var token = window.byted_acrawler.sign({url: url});
-            
-            return token;
-            }"""
-        )
-
-        url = "{}&_signature={}".format(url, evaluatedPage)
-
-        if calc_tt_params:
-            page.add_script_tag(content=get_tt_params_script())
-
-            tt_params = page.evaluate(
-                """() => {
-                    return window.genXTTParams("""
-                + json.dumps(dict(parse_qsl(splitquery(url)[1])))
-                + """);
-            
-                }"""
-            )
-
-        context.close()
-        return (verifyFp, device_id, evaluatedPage, tt_params)
-
-    def clean_up(self):
-        try:
-            self.browser.close()
-        except Exception:
-            logging.info("cleanup failed")
-        # playwright.stop()
-
-    def find_redirect(self, url):
-        self.page.goto(url, {"waitUntil": "load"})
-        self.redirect_url = self.page.url
-
-    def __format_proxy(self, proxy):
-        if proxy is not None:
-            return {"http": proxy, "https": proxy}
-        else:
-            return None
-
-    def __get_js(self):
-        return requests.get(
-            "https://sf16-muse-va.ibytedtos.com/obj/rc-web-sdk-gcs/acrawler.js",
-            proxies=self.__format_proxy(self.proxy),
-        ).text
-
- -
- -
-
-
#   - - - def - get_playwright(): -
- -
- View Source -
def get_playwright():
-    global playwright
-    if playwright is None:
-        try:
-            playwright = sync_playwright().start()
-        except Exception as e:
-            raise e
-
-    return playwright
-
- -
- - - -
-
- - -
- View Source -
class browser(BrowserInterface):
-    def __init__(
-        self,
-        **kwargs,
-    ):
-        self.kwargs = kwargs
-        self.debug = kwargs.get("debug", False)
-        self.proxy = kwargs.get("proxy", None)
-        self.api_url = kwargs.get("api_url", None)
-        self.referrer = kwargs.get("referer", "https://www.tiktok.com/")
-        self.language = kwargs.get("language", "en")
-        self.executablePath = kwargs.get("executablePath", None)
-        self.device_id = kwargs.get("custom_device_id", None)
-
-        args = kwargs.get("browser_args", [])
-        options = kwargs.get("browser_options", {})
-
-        if len(args) == 0:
-            self.args = []
-        else:
-            self.args = args
-
-        self.options = {
-            "headless": True,
-            "handle_sigint": True,
-            "handle_sigterm": True,
-            "handle_sighup": True,
-        }
-
-        if self.proxy is not None:
-            if "@" in self.proxy:
-                server_prefix = self.proxy.split("://")[0]
-                address = self.proxy.split("@")[1]
-                self.options["proxy"] = {
-                    "server": server_prefix + "://" + address,
-                    "username": self.proxy.split("://")[1].split(":")[0],
-                    "password": self.proxy.split("://")[1].split("@")[0].split(":")[1],
-                }
-            else:
-                self.options["proxy"] = {"server": self.proxy}
-
-        self.options.update(options)
-
-        if self.executablePath is not None:
-            self.options["executablePath"] = self.executablePath
-
-        try:
-            self.browser = get_playwright().webkit.launch(
-                args=self.args, **self.options
-            )
-        except Exception as e:
-            logging.critical(e)
-            raise e
-
-        context = self.create_context(set_useragent=True)
-        page = context.new_page()
-        self.get_params(page)
-        context.close()
-
-    def get_params(self, page) -> None:
-        self.browser_language = self.kwargs.get(
-            "browser_language",
-            page.evaluate("""() => { return navigator.language; }"""),
-        )
-        self.browser_version = page.evaluate(
-            """() => { return window.navigator.appVersion; }"""
-        )
-
-        if len(self.browser_language.split("-")) == 0:
-            self.region = self.kwargs.get("region", "US")
-            self.language = self.kwargs.get("language", "en")
-        elif len(self.browser_language.split("-")) == 1:
-            self.region = self.kwargs.get("region", "US")
-            self.language = self.browser_language.split("-")[0]
-        else:
-            self.region = self.kwargs.get("region", self.browser_language.split("-")[1])
-            self.language = self.kwargs.get(
-                "language", self.browser_language.split("-")[0]
-            )
-
-        self.timezone_name = self.kwargs.get(
-            "timezone_name",
-            page.evaluate(
-                """() => { return Intl.DateTimeFormat().resolvedOptions().timeZone; }"""
-            ),
-        )
-        self.width = page.evaluate("""() => { return screen.width; }""")
-        self.height = page.evaluate("""() => { return screen.height; }""")
-
-    def create_context(self, set_useragent=False):
-        iphone = playwright.devices["iPhone 11 Pro"]
-        iphone["viewport"] = {
-            "width": random.randint(320, 1920),
-            "height": random.randint(320, 1920),
-        }
-        iphone["device_scale_factor"] = random.randint(1, 3)
-        iphone["is_mobile"] = random.randint(1, 2) == 1
-        iphone["has_touch"] = random.randint(1, 2) == 1
-
-        iphone["bypass_csp"] = True
-
-        context = self.browser.new_context(**iphone)
-        if set_useragent:
-            self.userAgent = iphone["user_agent"]
-
-        return context
-
-    def base36encode(self, number, alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
-        """Converts an integer to a base36 string."""
-        base36 = ""
-        sign = ""
-
-        if number < 0:
-            sign = "-"
-            number = -number
-
-        if 0 <= number < len(alphabet):
-            return sign + alphabet[number]
-
-        while number != 0:
-            number, i = divmod(number, len(alphabet))
-            base36 = alphabet[i] + base36
-
-        return sign + base36
-
-    def gen_verifyFp(self):
-        chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[:]
-        chars_len = len(chars)
-        scenario_title = self.base36encode(int(time.time() * 1000))
-        uuid = [0] * 36
-        uuid[8] = "_"
-        uuid[13] = "_"
-        uuid[18] = "_"
-        uuid[23] = "_"
-        uuid[14] = "4"
-
-        for i in range(36):
-            if uuid[i] != 0:
-                continue
-            r = int(random.random() * chars_len)
-            uuid[i] = chars[int((3 & r) | 8 if i == 19 else r)]
-
-        return f'verify_{scenario_title.lower()}_{"".join(uuid)}'
-
-    def sign_url(self, calc_tt_params=False, **kwargs):
-        def process(route):
-            route.abort()
-
-        url = kwargs.get("url", None)
-        if url is None:
-            raise Exception("sign_url required a url parameter")
-
-        tt_params = None
-        context = self.create_context()
-        page = context.new_page()
-
-        if calc_tt_params:
-            page.route(re.compile(r"(\.png)|(\.jpeg)|(\.mp4)|(x-expire)"), process)
-            page.goto(
-                kwargs.get("default_url", "https://www.tiktok.com/@redbull"),
-                wait_until="load",
-            )
-
-        verifyFp = "".join(
-            random.choice(
-                string.ascii_lowercase + string.ascii_uppercase + string.digits
-            )
-            for i in range(16)
-        )
-        if kwargs.get("gen_new_verifyFp", False):
-            verifyFp = self.gen_verifyFp()
-        else:
-            verifyFp = kwargs.get(
-                "custom_verifyFp",
-                "verify_khgp4f49_V12d4mRX_MdCO_4Wzt_Ar0k_z4RCQC9pUDpX",
-            )
-
-        if kwargs.get("custom_device_id") is not None:
-            device_id = kwargs.get("custom_device_id", None)
-        elif self.device_id is None:
-            device_id = str(random.randint(10000, 999999999))
-        else:
-            device_id = self.device_id
-
-        url = "{}&verifyFp={}&device_id={}".format(url, verifyFp, device_id)
-
-        page.add_script_tag(content=get_acrawler())
-        evaluatedPage = page.evaluate(
-            '''() => {
-            var url = "'''
-            + url
-            + """"
-            var token = window.byted_acrawler.sign({url: url});
-            
-            return token;
-            }"""
-        )
-
-        url = "{}&_signature={}".format(url, evaluatedPage)
-
-        if calc_tt_params:
-            page.add_script_tag(content=get_tt_params_script())
-
-            tt_params = page.evaluate(
-                """() => {
-                    return window.genXTTParams("""
-                + json.dumps(dict(parse_qsl(splitquery(url)[1])))
-                + """);
-            
-                }"""
-            )
-
-        context.close()
-        return (verifyFp, device_id, evaluatedPage, tt_params)
-
-    def clean_up(self):
-        try:
-            self.browser.close()
-        except Exception:
-            logging.info("cleanup failed")
-        # playwright.stop()
-
-    def find_redirect(self, url):
-        self.page.goto(url, {"waitUntil": "load"})
-        self.redirect_url = self.page.url
-
-    def __format_proxy(self, proxy):
-        if proxy is not None:
-            return {"http": proxy, "https": proxy}
-        else:
-            return None
-
-    def __get_js(self):
-        return requests.get(
-            "https://sf16-muse-va.ibytedtos.com/obj/rc-web-sdk-gcs/acrawler.js",
-            proxies=self.__format_proxy(self.proxy),
-        ).text
-
- -
- -

Helper class that provides a standard way to create an ABC using -inheritance.

-
- - -
-
#   - - - browser(**kwargs) -
- -
- View Source -
    def __init__(
-        self,
-        **kwargs,
-    ):
-        self.kwargs = kwargs
-        self.debug = kwargs.get("debug", False)
-        self.proxy = kwargs.get("proxy", None)
-        self.api_url = kwargs.get("api_url", None)
-        self.referrer = kwargs.get("referer", "https://www.tiktok.com/")
-        self.language = kwargs.get("language", "en")
-        self.executablePath = kwargs.get("executablePath", None)
-        self.device_id = kwargs.get("custom_device_id", None)
-
-        args = kwargs.get("browser_args", [])
-        options = kwargs.get("browser_options", {})
-
-        if len(args) == 0:
-            self.args = []
-        else:
-            self.args = args
-
-        self.options = {
-            "headless": True,
-            "handle_sigint": True,
-            "handle_sigterm": True,
-            "handle_sighup": True,
-        }
-
-        if self.proxy is not None:
-            if "@" in self.proxy:
-                server_prefix = self.proxy.split("://")[0]
-                address = self.proxy.split("@")[1]
-                self.options["proxy"] = {
-                    "server": server_prefix + "://" + address,
-                    "username": self.proxy.split("://")[1].split(":")[0],
-                    "password": self.proxy.split("://")[1].split("@")[0].split(":")[1],
-                }
-            else:
-                self.options["proxy"] = {"server": self.proxy}
-
-        self.options.update(options)
-
-        if self.executablePath is not None:
-            self.options["executablePath"] = self.executablePath
-
-        try:
-            self.browser = get_playwright().webkit.launch(
-                args=self.args, **self.options
-            )
-        except Exception as e:
-            logging.critical(e)
-            raise e
-
-        context = self.create_context(set_useragent=True)
-        page = context.new_page()
-        self.get_params(page)
-        context.close()
-
- -
- - - -
-
-
#   - - - def - get_params(self, page) -> None: -
- -
- View Source -
    def get_params(self, page) -> None:
-        self.browser_language = self.kwargs.get(
-            "browser_language",
-            page.evaluate("""() => { return navigator.language; }"""),
-        )
-        self.browser_version = page.evaluate(
-            """() => { return window.navigator.appVersion; }"""
-        )
-
-        if len(self.browser_language.split("-")) == 0:
-            self.region = self.kwargs.get("region", "US")
-            self.language = self.kwargs.get("language", "en")
-        elif len(self.browser_language.split("-")) == 1:
-            self.region = self.kwargs.get("region", "US")
-            self.language = self.browser_language.split("-")[0]
-        else:
-            self.region = self.kwargs.get("region", self.browser_language.split("-")[1])
-            self.language = self.kwargs.get(
-                "language", self.browser_language.split("-")[0]
-            )
-
-        self.timezone_name = self.kwargs.get(
-            "timezone_name",
-            page.evaluate(
-                """() => { return Intl.DateTimeFormat().resolvedOptions().timeZone; }"""
-            ),
-        )
-        self.width = page.evaluate("""() => { return screen.width; }""")
-        self.height = page.evaluate("""() => { return screen.height; }""")
-
- -
- - - -
-
-
#   - - - def - create_context(self, set_useragent=False): -
- -
- View Source -
    def create_context(self, set_useragent=False):
-        iphone = playwright.devices["iPhone 11 Pro"]
-        iphone["viewport"] = {
-            "width": random.randint(320, 1920),
-            "height": random.randint(320, 1920),
-        }
-        iphone["device_scale_factor"] = random.randint(1, 3)
-        iphone["is_mobile"] = random.randint(1, 2) == 1
-        iphone["has_touch"] = random.randint(1, 2) == 1
-
-        iphone["bypass_csp"] = True
-
-        context = self.browser.new_context(**iphone)
-        if set_useragent:
-            self.userAgent = iphone["user_agent"]
-
-        return context
-
- -
- - - -
-
-
#   - - - def - base36encode(self, number, alphabet='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'): -
- -
- View Source -
    def base36encode(self, number, alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
-        """Converts an integer to a base36 string."""
-        base36 = ""
-        sign = ""
-
-        if number < 0:
-            sign = "-"
-            number = -number
-
-        if 0 <= number < len(alphabet):
-            return sign + alphabet[number]
-
-        while number != 0:
-            number, i = divmod(number, len(alphabet))
-            base36 = alphabet[i] + base36
-
-        return sign + base36
-
- -
- -

Converts an integer to a base36 string.

-
- - -
-
-
#   - - - def - gen_verifyFp(self): -
- -
- View Source -
    def gen_verifyFp(self):
-        chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[:]
-        chars_len = len(chars)
-        scenario_title = self.base36encode(int(time.time() * 1000))
-        uuid = [0] * 36
-        uuid[8] = "_"
-        uuid[13] = "_"
-        uuid[18] = "_"
-        uuid[23] = "_"
-        uuid[14] = "4"
-
-        for i in range(36):
-            if uuid[i] != 0:
-                continue
-            r = int(random.random() * chars_len)
-            uuid[i] = chars[int((3 & r) | 8 if i == 19 else r)]
-
-        return f'verify_{scenario_title.lower()}_{"".join(uuid)}'
-
- -
- - - -
-
-
#   - - - def - sign_url(self, calc_tt_params=False, **kwargs): -
- -
- View Source -
    def sign_url(self, calc_tt_params=False, **kwargs):
-        def process(route):
-            route.abort()
-
-        url = kwargs.get("url", None)
-        if url is None:
-            raise Exception("sign_url required a url parameter")
-
-        tt_params = None
-        context = self.create_context()
-        page = context.new_page()
-
-        if calc_tt_params:
-            page.route(re.compile(r"(\.png)|(\.jpeg)|(\.mp4)|(x-expire)"), process)
-            page.goto(
-                kwargs.get("default_url", "https://www.tiktok.com/@redbull"),
-                wait_until="load",
-            )
-
-        verifyFp = "".join(
-            random.choice(
-                string.ascii_lowercase + string.ascii_uppercase + string.digits
-            )
-            for i in range(16)
-        )
-        if kwargs.get("gen_new_verifyFp", False):
-            verifyFp = self.gen_verifyFp()
-        else:
-            verifyFp = kwargs.get(
-                "custom_verifyFp",
-                "verify_khgp4f49_V12d4mRX_MdCO_4Wzt_Ar0k_z4RCQC9pUDpX",
-            )
-
-        if kwargs.get("custom_device_id") is not None:
-            device_id = kwargs.get("custom_device_id", None)
-        elif self.device_id is None:
-            device_id = str(random.randint(10000, 999999999))
-        else:
-            device_id = self.device_id
-
-        url = "{}&verifyFp={}&device_id={}".format(url, verifyFp, device_id)
-
-        page.add_script_tag(content=get_acrawler())
-        evaluatedPage = page.evaluate(
-            '''() => {
-            var url = "'''
-            + url
-            + """"
-            var token = window.byted_acrawler.sign({url: url});
-            
-            return token;
-            }"""
-        )
-
-        url = "{}&_signature={}".format(url, evaluatedPage)
-
-        if calc_tt_params:
-            page.add_script_tag(content=get_tt_params_script())
-
-            tt_params = page.evaluate(
-                """() => {
-                    return window.genXTTParams("""
-                + json.dumps(dict(parse_qsl(splitquery(url)[1])))
-                + """);
-            
-                }"""
-            )
-
-        context.close()
-        return (verifyFp, device_id, evaluatedPage, tt_params)
-
- -
- - - -
-
-
#   - - - def - clean_up(self): -
- -
- View Source -
    def clean_up(self):
-        try:
-            self.browser.close()
-        except Exception:
-            logging.info("cleanup failed")
-        # playwright.stop()
-
- -
- - - -
-
-
#   - - - def - find_redirect(self, url): -
- -
- View Source -
    def find_redirect(self, url):
-        self.page.goto(url, {"waitUntil": "load"})
-        self.redirect_url = self.page.url
-
- -
- - - -
-
-
- - - - \ No newline at end of file diff --git a/docs/TikTokApi/browser_utilities/browser_interface.html b/docs/TikTokApi/browser_utilities/browser_interface.html deleted file mode 100644 index 23d82290..00000000 --- a/docs/TikTokApi/browser_utilities/browser_interface.html +++ /dev/null @@ -1,384 +0,0 @@ - - - - - - - TikTokApi.browser_utilities.browser_interface API documentation - - - - - - - - -
-
-

-TikTokApi.browser_utilities.browser_interface

- - -
- View Source -
import abc
-
-
-class BrowserInterface(abc.ABC):
-    @abc.abstractmethod
-    def __init__(self, **kwargs):
-        pass
-
-    @abc.abstractmethod
-    def get_params(self, page) -> None:
-        pass
-
-    # Returns verify_fp, device_id, signature, tt_params
-    @abc.abstractmethod
-    def sign_url(self, calc_tt_params=False, **kwargs):
-        pass
-
-    @abc.abstractmethod
-    def clean_up(self) -> None:
-        pass
-
- -
- -
-
-
- #   - - - class - BrowserInterface(abc.ABC): -
- -
- View Source -
class BrowserInterface(abc.ABC):
-    @abc.abstractmethod
-    def __init__(self, **kwargs):
-        pass
-
-    @abc.abstractmethod
-    def get_params(self, page) -> None:
-        pass
-
-    # Returns verify_fp, device_id, signature, tt_params
-    @abc.abstractmethod
-    def sign_url(self, calc_tt_params=False, **kwargs):
-        pass
-
-    @abc.abstractmethod
-    def clean_up(self) -> None:
-        pass
-
- -
- -

Helper class that provides a standard way to create an ABC using -inheritance.

-
- - -
-
#   - -
@abc.abstractmethod
- - def - get_params(self, page) -> None: -
- -
- View Source -
    @abc.abstractmethod
-    def get_params(self, page) -> None:
-        pass
-
- -
- - - -
-
-
#   - -
@abc.abstractmethod
- - def - sign_url(self, calc_tt_params=False, **kwargs): -
- -
- View Source -
    @abc.abstractmethod
-    def sign_url(self, calc_tt_params=False, **kwargs):
-        pass
-
- -
- - - -
-
-
#   - -
@abc.abstractmethod
- - def - clean_up(self) -> None: -
- -
- View Source -
    @abc.abstractmethod
-    def clean_up(self) -> None:
-        pass
-
- -
- - - -
-
-
- - - - \ No newline at end of file diff --git a/docs/TikTokApi/browser_utilities/browser_selenium.html b/docs/TikTokApi/browser_utilities/browser_selenium.html deleted file mode 100644 index 79d04ee1..00000000 --- a/docs/TikTokApi/browser_utilities/browser_selenium.html +++ /dev/null @@ -1,1037 +0,0 @@ - - - - - - - TikTokApi.browser_utilities.browser_selenium API documentation - - - - - - - - -
-
-

-TikTokApi.browser_utilities.browser_selenium

- - -
- View Source -
import random
-import time
-import requests
-import logging
-from threading import Thread
-import time
-import re
-import random
-import json
-from .browser_interface import BrowserInterface
-from selenium_stealth import stealth
-from selenium import webdriver
-from .get_acrawler import get_acrawler, get_tt_params_script
-from urllib.parse import splitquery, parse_qs, parse_qsl
-
-
-class browser(BrowserInterface):
-    def __init__(
-        self,
-        **kwargs,
-    ):
-        self.kwargs = kwargs
-        self.debug = kwargs.get("debug", False)
-        self.proxy = kwargs.get("proxy", None)
-        self.api_url = kwargs.get("api_url", None)
-        self.referrer = kwargs.get("referer", "https://www.tiktok.com/")
-        self.language = kwargs.get("language", "en")
-        self.executablePath = kwargs.get("executablePath", "chromedriver")
-        self.device_id = kwargs.get("custom_device_id", None)
-
-        args = kwargs.get("browser_args", [])
-        options = kwargs.get("browser_options", {})
-
-        if len(args) == 0:
-            self.args = []
-        else:
-            self.args = args
-
-        options = webdriver.ChromeOptions()
-        options.add_argument("--headless")
-        options.add_argument("log-level=2")
-        self.options = {
-            "headless": True,
-            "handleSIGINT": True,
-            "handleSIGTERM": True,
-            "handleSIGHUP": True,
-        }
-
-        if self.proxy is not None:
-            if "@" in self.proxy:
-                server_prefix = self.proxy.split("://")[0]
-                address = self.proxy.split("@")[1]
-                self.options["proxy"] = {
-                    "server": server_prefix + "://" + address,
-                    "username": self.proxy.split("://")[1].split(":")[0],
-                    "password": self.proxy.split("://")[1].split("@")[0].split(":")[1],
-                }
-            else:
-                self.options["proxy"] = {"server": self.proxy}
-
-        # self.options.update(options)
-
-        if self.executablePath is not None:
-            self.options["executablePath"] = self.executablePath
-
-        try:
-            self.browser = webdriver.Chrome(
-                executable_path=self.executablePath, chrome_options=options
-            )
-        except Exception as e:
-            raise e
-
-        # Page avoidance
-        self.setup_browser()
-        # page.close()
-
-    def setup_browser(self):
-        stealth(
-            self.browser,
-            languages=["en-US", "en"],
-            vendor="Google Inc.",
-            platform="Win32",
-            webgl_vendor="Intel Inc.",
-            renderer="Intel Iris OpenGL Engine",
-            fix_hairline=True,
-        )
-
-        self.get_params(self.browser)
-        # NOTE: Slower than playwright at loading this because playwright can ignore unneeded files.
-        self.browser.get("https://www.tiktok.com/@redbull")
-        self.browser.execute_script(get_acrawler())
-        self.browser.execute_script(get_tt_params_script())
-
-    def get_params(self, page) -> None:
-        self.userAgent = page.execute_script("""return navigator.userAgent""")
-        self.browser_language = self.kwargs.get(
-            "browser_language", ("""return navigator.language""")
-        )
-        self.browser_version = """return window.navigator.appVersion"""
-
-        if len(self.browser_language.split("-")) == 0:
-            self.region = self.kwargs.get("region", "US")
-            self.language = self.kwargs.get("language", "en")
-        elif len(self.browser_language.split("-")) == 1:
-            self.region = self.kwargs.get("region", "US")
-            self.language = self.browser_language.split("-")[0]
-        else:
-            self.region = self.kwargs.get("region", self.browser_language.split("-")[1])
-            self.language = self.kwargs.get(
-                "language", self.browser_language.split("-")[0]
-            )
-
-        self.timezone_name = self.kwargs.get(
-            "timezone_name",
-            ("""return Intl.DateTimeFormat().resolvedOptions().timeZone"""),
-        )
-        self.width = """return screen.width"""
-        self.height = """return screen.height"""
-
-    def base36encode(self, number, alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
-        """Converts an integer to a base36 string."""
-        base36 = ""
-        sign = ""
-
-        if number < 0:
-            sign = "-"
-            number = -number
-
-        if 0 <= number < len(alphabet):
-            return sign + alphabet[number]
-
-        while number != 0:
-            number, i = divmod(number, len(alphabet))
-            base36 = alphabet[i] + base36
-
-        return sign + base36
-
-    def gen_verifyFp(self):
-        chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[:]
-        chars_len = len(chars)
-        scenario_title = self.base36encode(int(time.time() * 1000))
-        uuid = [0] * 36
-        uuid[8] = "_"
-        uuid[13] = "_"
-        uuid[18] = "_"
-        uuid[23] = "_"
-        uuid[14] = "4"
-
-        for i in range(36):
-            if uuid[i] != 0:
-                continue
-            r = int(random.random() * chars_len)
-            uuid[i] = chars[int((3 & r) | 8 if i == 19 else r)]
-
-        return f'verify_{scenario_title.lower()}_{"".join(uuid)}'
-
-    def sign_url(self, calc_tt_params=False, **kwargs):
-        url = kwargs.get("url", None)
-        if url is None:
-            raise Exception("sign_url required a url parameter")
-
-        tt_params = None
-        if kwargs.get("gen_new_verifyFp", False):
-            verifyFp = self.gen_verifyFp()
-        else:
-            verifyFp = kwargs.get(
-                "custom_verifyFp",
-                "verify_khgp4f49_V12d4mRX_MdCO_4Wzt_Ar0k_z4RCQC9pUDpX",
-            )
-
-        if kwargs.get("custom_device_id") is not None:
-            device_id = kwargs.get("custom_device_id", None)
-        elif self.device_id is None:
-            device_id = str(random.randint(10000, 999999999))
-        else:
-            device_id = self.device_id
-
-        url = "{}&verifyFp={}&device_id={}".format(url, verifyFp, device_id)
-        # self.browser.execute_script(content=get_acrawler())
-        # Should be covered by an earlier addition of get_acrawler.
-        evaluatedPage = (
-            self.browser.execute_script(
-                '''
-        var url = "'''
-                + url
-                + """"
-        var token = window.byted_acrawler.sign({url: url});
-        return token;
-        """
-            ),
-        )
-
-        url = "{}&_signature={}".format(url, evaluatedPage)
-        # self.browser.execute_script(content=get_tt_params_script())
-        # Should be covered by an earlier addition of get_acrawler.
-
-        tt_params = self.browser.execute_script(
-            """() => {
-                return window.genXTTParams("""
-            + json.dumps(dict(parse_qsl(splitquery(url)[1])))
-            + """);
-        
-            }"""
-        )
-
-        return (verifyFp, device_id, evaluatedPage, tt_params)
-
-    def clean_up(self):
-        try:
-            self.browser.close()
-        except:
-            logging.warning("cleanup of browser failed")
-
-    def __format_proxy(self, proxy):
-        if proxy is not None:
-            return {"http": proxy, "https": proxy}
-        else:
-            return None
-
-    def __get_js(self):
-        return requests.get(
-            "https://sf16-muse-va.ibytedtos.com/obj/rc-web-sdk-gcs/acrawler.js",
-            proxies=self.__format_proxy(self.proxy),
-        ).text
-
- -
- -
-
- - -
- View Source -
class browser(BrowserInterface):
-    def __init__(
-        self,
-        **kwargs,
-    ):
-        self.kwargs = kwargs
-        self.debug = kwargs.get("debug", False)
-        self.proxy = kwargs.get("proxy", None)
-        self.api_url = kwargs.get("api_url", None)
-        self.referrer = kwargs.get("referer", "https://www.tiktok.com/")
-        self.language = kwargs.get("language", "en")
-        self.executablePath = kwargs.get("executablePath", "chromedriver")
-        self.device_id = kwargs.get("custom_device_id", None)
-
-        args = kwargs.get("browser_args", [])
-        options = kwargs.get("browser_options", {})
-
-        if len(args) == 0:
-            self.args = []
-        else:
-            self.args = args
-
-        options = webdriver.ChromeOptions()
-        options.add_argument("--headless")
-        options.add_argument("log-level=2")
-        self.options = {
-            "headless": True,
-            "handleSIGINT": True,
-            "handleSIGTERM": True,
-            "handleSIGHUP": True,
-        }
-
-        if self.proxy is not None:
-            if "@" in self.proxy:
-                server_prefix = self.proxy.split("://")[0]
-                address = self.proxy.split("@")[1]
-                self.options["proxy"] = {
-                    "server": server_prefix + "://" + address,
-                    "username": self.proxy.split("://")[1].split(":")[0],
-                    "password": self.proxy.split("://")[1].split("@")[0].split(":")[1],
-                }
-            else:
-                self.options["proxy"] = {"server": self.proxy}
-
-        # self.options.update(options)
-
-        if self.executablePath is not None:
-            self.options["executablePath"] = self.executablePath
-
-        try:
-            self.browser = webdriver.Chrome(
-                executable_path=self.executablePath, chrome_options=options
-            )
-        except Exception as e:
-            raise e
-
-        # Page avoidance
-        self.setup_browser()
-        # page.close()
-
-    def setup_browser(self):
-        stealth(
-            self.browser,
-            languages=["en-US", "en"],
-            vendor="Google Inc.",
-            platform="Win32",
-            webgl_vendor="Intel Inc.",
-            renderer="Intel Iris OpenGL Engine",
-            fix_hairline=True,
-        )
-
-        self.get_params(self.browser)
-        # NOTE: Slower than playwright at loading this because playwright can ignore unneeded files.
-        self.browser.get("https://www.tiktok.com/@redbull")
-        self.browser.execute_script(get_acrawler())
-        self.browser.execute_script(get_tt_params_script())
-
-    def get_params(self, page) -> None:
-        self.userAgent = page.execute_script("""return navigator.userAgent""")
-        self.browser_language = self.kwargs.get(
-            "browser_language", ("""return navigator.language""")
-        )
-        self.browser_version = """return window.navigator.appVersion"""
-
-        if len(self.browser_language.split("-")) == 0:
-            self.region = self.kwargs.get("region", "US")
-            self.language = self.kwargs.get("language", "en")
-        elif len(self.browser_language.split("-")) == 1:
-            self.region = self.kwargs.get("region", "US")
-            self.language = self.browser_language.split("-")[0]
-        else:
-            self.region = self.kwargs.get("region", self.browser_language.split("-")[1])
-            self.language = self.kwargs.get(
-                "language", self.browser_language.split("-")[0]
-            )
-
-        self.timezone_name = self.kwargs.get(
-            "timezone_name",
-            ("""return Intl.DateTimeFormat().resolvedOptions().timeZone"""),
-        )
-        self.width = """return screen.width"""
-        self.height = """return screen.height"""
-
-    def base36encode(self, number, alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
-        """Converts an integer to a base36 string."""
-        base36 = ""
-        sign = ""
-
-        if number < 0:
-            sign = "-"
-            number = -number
-
-        if 0 <= number < len(alphabet):
-            return sign + alphabet[number]
-
-        while number != 0:
-            number, i = divmod(number, len(alphabet))
-            base36 = alphabet[i] + base36
-
-        return sign + base36
-
-    def gen_verifyFp(self):
-        chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[:]
-        chars_len = len(chars)
-        scenario_title = self.base36encode(int(time.time() * 1000))
-        uuid = [0] * 36
-        uuid[8] = "_"
-        uuid[13] = "_"
-        uuid[18] = "_"
-        uuid[23] = "_"
-        uuid[14] = "4"
-
-        for i in range(36):
-            if uuid[i] != 0:
-                continue
-            r = int(random.random() * chars_len)
-            uuid[i] = chars[int((3 & r) | 8 if i == 19 else r)]
-
-        return f'verify_{scenario_title.lower()}_{"".join(uuid)}'
-
-    def sign_url(self, calc_tt_params=False, **kwargs):
-        url = kwargs.get("url", None)
-        if url is None:
-            raise Exception("sign_url required a url parameter")
-
-        tt_params = None
-        if kwargs.get("gen_new_verifyFp", False):
-            verifyFp = self.gen_verifyFp()
-        else:
-            verifyFp = kwargs.get(
-                "custom_verifyFp",
-                "verify_khgp4f49_V12d4mRX_MdCO_4Wzt_Ar0k_z4RCQC9pUDpX",
-            )
-
-        if kwargs.get("custom_device_id") is not None:
-            device_id = kwargs.get("custom_device_id", None)
-        elif self.device_id is None:
-            device_id = str(random.randint(10000, 999999999))
-        else:
-            device_id = self.device_id
-
-        url = "{}&verifyFp={}&device_id={}".format(url, verifyFp, device_id)
-        # self.browser.execute_script(content=get_acrawler())
-        # Should be covered by an earlier addition of get_acrawler.
-        evaluatedPage = (
-            self.browser.execute_script(
-                '''
-        var url = "'''
-                + url
-                + """"
-        var token = window.byted_acrawler.sign({url: url});
-        return token;
-        """
-            ),
-        )
-
-        url = "{}&_signature={}".format(url, evaluatedPage)
-        # self.browser.execute_script(content=get_tt_params_script())
-        # Should be covered by an earlier addition of get_acrawler.
-
-        tt_params = self.browser.execute_script(
-            """() => {
-                return window.genXTTParams("""
-            + json.dumps(dict(parse_qsl(splitquery(url)[1])))
-            + """);
-        
-            }"""
-        )
-
-        return (verifyFp, device_id, evaluatedPage, tt_params)
-
-    def clean_up(self):
-        try:
-            self.browser.close()
-        except:
-            logging.warning("cleanup of browser failed")
-
-    def __format_proxy(self, proxy):
-        if proxy is not None:
-            return {"http": proxy, "https": proxy}
-        else:
-            return None
-
-    def __get_js(self):
-        return requests.get(
-            "https://sf16-muse-va.ibytedtos.com/obj/rc-web-sdk-gcs/acrawler.js",
-            proxies=self.__format_proxy(self.proxy),
-        ).text
-
- -
- -

Helper class that provides a standard way to create an ABC using -inheritance.

-
- - -
-
#   - - - browser(**kwargs) -
- -
- View Source -
    def __init__(
-        self,
-        **kwargs,
-    ):
-        self.kwargs = kwargs
-        self.debug = kwargs.get("debug", False)
-        self.proxy = kwargs.get("proxy", None)
-        self.api_url = kwargs.get("api_url", None)
-        self.referrer = kwargs.get("referer", "https://www.tiktok.com/")
-        self.language = kwargs.get("language", "en")
-        self.executablePath = kwargs.get("executablePath", "chromedriver")
-        self.device_id = kwargs.get("custom_device_id", None)
-
-        args = kwargs.get("browser_args", [])
-        options = kwargs.get("browser_options", {})
-
-        if len(args) == 0:
-            self.args = []
-        else:
-            self.args = args
-
-        options = webdriver.ChromeOptions()
-        options.add_argument("--headless")
-        options.add_argument("log-level=2")
-        self.options = {
-            "headless": True,
-            "handleSIGINT": True,
-            "handleSIGTERM": True,
-            "handleSIGHUP": True,
-        }
-
-        if self.proxy is not None:
-            if "@" in self.proxy:
-                server_prefix = self.proxy.split("://")[0]
-                address = self.proxy.split("@")[1]
-                self.options["proxy"] = {
-                    "server": server_prefix + "://" + address,
-                    "username": self.proxy.split("://")[1].split(":")[0],
-                    "password": self.proxy.split("://")[1].split("@")[0].split(":")[1],
-                }
-            else:
-                self.options["proxy"] = {"server": self.proxy}
-
-        # self.options.update(options)
-
-        if self.executablePath is not None:
-            self.options["executablePath"] = self.executablePath
-
-        try:
-            self.browser = webdriver.Chrome(
-                executable_path=self.executablePath, chrome_options=options
-            )
-        except Exception as e:
-            raise e
-
-        # Page avoidance
-        self.setup_browser()
-        # page.close()
-
- -
- - - -
-
-
#   - - - def - setup_browser(self): -
- -
- View Source -
    def setup_browser(self):
-        stealth(
-            self.browser,
-            languages=["en-US", "en"],
-            vendor="Google Inc.",
-            platform="Win32",
-            webgl_vendor="Intel Inc.",
-            renderer="Intel Iris OpenGL Engine",
-            fix_hairline=True,
-        )
-
-        self.get_params(self.browser)
-        # NOTE: Slower than playwright at loading this because playwright can ignore unneeded files.
-        self.browser.get("https://www.tiktok.com/@redbull")
-        self.browser.execute_script(get_acrawler())
-        self.browser.execute_script(get_tt_params_script())
-
- -
- - - -
-
-
#   - - - def - get_params(self, page) -> None: -
- -
- View Source -
    def get_params(self, page) -> None:
-        self.userAgent = page.execute_script("""return navigator.userAgent""")
-        self.browser_language = self.kwargs.get(
-            "browser_language", ("""return navigator.language""")
-        )
-        self.browser_version = """return window.navigator.appVersion"""
-
-        if len(self.browser_language.split("-")) == 0:
-            self.region = self.kwargs.get("region", "US")
-            self.language = self.kwargs.get("language", "en")
-        elif len(self.browser_language.split("-")) == 1:
-            self.region = self.kwargs.get("region", "US")
-            self.language = self.browser_language.split("-")[0]
-        else:
-            self.region = self.kwargs.get("region", self.browser_language.split("-")[1])
-            self.language = self.kwargs.get(
-                "language", self.browser_language.split("-")[0]
-            )
-
-        self.timezone_name = self.kwargs.get(
-            "timezone_name",
-            ("""return Intl.DateTimeFormat().resolvedOptions().timeZone"""),
-        )
-        self.width = """return screen.width"""
-        self.height = """return screen.height"""
-
- -
- - - -
-
-
#   - - - def - base36encode(self, number, alphabet='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'): -
- -
- View Source -
    def base36encode(self, number, alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
-        """Converts an integer to a base36 string."""
-        base36 = ""
-        sign = ""
-
-        if number < 0:
-            sign = "-"
-            number = -number
-
-        if 0 <= number < len(alphabet):
-            return sign + alphabet[number]
-
-        while number != 0:
-            number, i = divmod(number, len(alphabet))
-            base36 = alphabet[i] + base36
-
-        return sign + base36
-
- -
- -

Converts an integer to a base36 string.

-
- - -
-
-
#   - - - def - gen_verifyFp(self): -
- -
- View Source -
    def gen_verifyFp(self):
-        chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[:]
-        chars_len = len(chars)
-        scenario_title = self.base36encode(int(time.time() * 1000))
-        uuid = [0] * 36
-        uuid[8] = "_"
-        uuid[13] = "_"
-        uuid[18] = "_"
-        uuid[23] = "_"
-        uuid[14] = "4"
-
-        for i in range(36):
-            if uuid[i] != 0:
-                continue
-            r = int(random.random() * chars_len)
-            uuid[i] = chars[int((3 & r) | 8 if i == 19 else r)]
-
-        return f'verify_{scenario_title.lower()}_{"".join(uuid)}'
-
- -
- - - -
-
-
#   - - - def - sign_url(self, calc_tt_params=False, **kwargs): -
- -
- View Source -
    def sign_url(self, calc_tt_params=False, **kwargs):
-        url = kwargs.get("url", None)
-        if url is None:
-            raise Exception("sign_url required a url parameter")
-
-        tt_params = None
-        if kwargs.get("gen_new_verifyFp", False):
-            verifyFp = self.gen_verifyFp()
-        else:
-            verifyFp = kwargs.get(
-                "custom_verifyFp",
-                "verify_khgp4f49_V12d4mRX_MdCO_4Wzt_Ar0k_z4RCQC9pUDpX",
-            )
-
-        if kwargs.get("custom_device_id") is not None:
-            device_id = kwargs.get("custom_device_id", None)
-        elif self.device_id is None:
-            device_id = str(random.randint(10000, 999999999))
-        else:
-            device_id = self.device_id
-
-        url = "{}&verifyFp={}&device_id={}".format(url, verifyFp, device_id)
-        # self.browser.execute_script(content=get_acrawler())
-        # Should be covered by an earlier addition of get_acrawler.
-        evaluatedPage = (
-            self.browser.execute_script(
-                '''
-        var url = "'''
-                + url
-                + """"
-        var token = window.byted_acrawler.sign({url: url});
-        return token;
-        """
-            ),
-        )
-
-        url = "{}&_signature={}".format(url, evaluatedPage)
-        # self.browser.execute_script(content=get_tt_params_script())
-        # Should be covered by an earlier addition of get_acrawler.
-
-        tt_params = self.browser.execute_script(
-            """() => {
-                return window.genXTTParams("""
-            + json.dumps(dict(parse_qsl(splitquery(url)[1])))
-            + """);
-        
-            }"""
-        )
-
-        return (verifyFp, device_id, evaluatedPage, tt_params)
-
- -
- - - -
-
-
#   - - - def - clean_up(self): -
- -
- View Source -
    def clean_up(self):
-        try:
-            self.browser.close()
-        except:
-            logging.warning("cleanup of browser failed")
-
- -
- - - -
-
-
- - - - \ No newline at end of file diff --git a/docs/TikTokApi/browser_utilities/get_acrawler.html b/docs/TikTokApi/browser_utilities/get_acrawler.html deleted file mode 100644 index 09d7163c..00000000 --- a/docs/TikTokApi/browser_utilities/get_acrawler.html +++ /dev/null @@ -1,298 +0,0 @@ - - - - - - - TikTokApi.browser_utilities.get_acrawler API documentation - - - - - - - - -
-
-

-TikTokApi.browser_utilities.get_acrawler

- - -
- View Source -
def get_tt_params_script():
-    return """var CryptoJS=CryptoJS||function(e,t){var r={},n=r.lib={},i=n.Base=function(){function e(){}return{extend:function(t){e.prototype=this;var r=new e;return t&&r.mixIn(t),r.hasOwnProperty("init")&&this.init!==r.init||(r.init=function(){r.$super.init.apply(this,arguments)}),r.init.prototype=r,r.$super=this,r},create:function(){var e=this.extend();return e.init.apply(e,arguments),e},init:function(){},mixIn:function(e){for(var t in e)e.hasOwnProperty(t)&&(this[t]=e[t]);e.hasOwnProperty("toString")&&(this.toString=e.toString)},clone:function(){return this.init.prototype.extend(this)}}}(),c=n.WordArray=i.extend({init:function(e,t){e=this.words=e||[],this.sigBytes=null!=t?t:4*e.length},toString:function(e){return(e||f).stringify(this)},concat:function(e){var t=this.words,r=e.words,n=this.sigBytes,i=e.sigBytes;if(this.clamp(),n%4)for(var c=0;c<i;c++){var o=r[c>>>2]>>>24-c%4*8&255;t[n+c>>>2]|=o<<24-(n+c)%4*8}else if(r.length>65535)for(c=0;c<i;c+=4)t[n+c>>>2]=r[c>>>2];else t.push.apply(t,r);return this.sigBytes+=i,this},clamp:function(){var t=this.words,r=this.sigBytes;t[r>>>2]&=4294967295<<32-r%4*8,t.length=e.ceil(r/4)},clone:function(){var e=i.clone.call(this);return e.words=this.words.slice(0),e},random:function(t){for(var r,n=[],i=function(t){t=t;var r=987654321,n=4294967295;return function(){var i=((r=36969*(65535&r)+(r>>16)&n)<<16)+(t=18e3*(65535&t)+(t>>16)&n)&n;return i/=4294967296,(i+=.5)*(e.random()>.5?1:-1)}},o=0;o<t;o+=4){var f=i(4294967296*(r||e.random()));r=987654071*f(),n.push(4294967296*f()|0)}return new c.init(n,t)}}),o=r.enc={},f=o.Hex={stringify:function(e){for(var t=e.words,r=e.sigBytes,n=[],i=0;i<r;i++){var c=t[i>>>2]>>>24-i%4*8&255;n.push((c>>>4).toString(16)),n.push((15&c).toString(16))}return n.join("")},parse:function(e){for(var t=e.length,r=[],n=0;n<t;n+=2)r[n>>>3]|=parseInt(e.substr(n,2),16)<<24-n%8*4;return new c.init(r,t/2)}},a=o.Latin1={stringify:function(e){for(var t=e.words,r=e.sigBytes,n=[],i=0;i<r;i++){var c=t[i>>>2]>>>24-i%4*8&255;n.push(String.fromCharCode(c))}return n.join("")},parse:function(e){for(var t=e.length,r=[],n=0;n<t;n++)r[n>>>2]|=(255&e.charCodeAt(n))<<24-n%4*8;return new c.init(r,t)}},s=o.Utf8={stringify:function(e){try{return decodeURIComponent(escape(a.stringify(e)))}catch(e){throw new Error("Malformed UTF-8 data")}},parse:function(e){return a.parse(unescape(encodeURIComponent(e)))}},u=n.BufferedBlockAlgorithm=i.extend({reset:function(){this._data=new c.init,this._nDataBytes=0},_append:function(e){"string"==typeof e&&(e=s.parse(e)),this._data.concat(e),this._nDataBytes+=e.sigBytes},_process:function(t){var r=this._data,n=r.words,i=r.sigBytes,o=this.blockSize,f=i/(4*o),a=(f=t?e.ceil(f):e.max((0|f)-this._minBufferSize,0))*o,s=e.min(4*a,i);if(a){for(var u=0;u<a;u+=o)this._doProcessBlock(n,u);var d=n.splice(0,a);r.sigBytes-=s}return new c.init(d,s)},clone:function(){var e=i.clone.call(this);return e._data=this._data.clone(),e},_minBufferSize:0}),d=(n.Hasher=u.extend({cfg:i.extend(),init:function(e){this.cfg=this.cfg.extend(e),this.reset()},reset:function(){u.reset.call(this),this._doReset()},update:function(e){return this._append(e),this._process(),this},finalize:function(e){return e&&this._append(e),this._doFinalize()},blockSize:16,_createHelper:function(e){return function(t,r){return new e.init(r).finalize(t)}},_createHmacHelper:function(e){return function(t,r){return new d.HMAC.init(e,r).finalize(t)}}}),r.algo={});return r}(Math);!function(){var e=CryptoJS,t=e.lib.WordArray;e.enc.Base64={stringify:function(e){var t=e.words,r=e.sigBytes,n=this._map;e.clamp();for(var i=[],c=0;c<r;c+=3)for(var o=(t[c>>>2]>>>24-c%4*8&255)<<16|(t[c+1>>>2]>>>24-(c+1)%4*8&255)<<8|t[c+2>>>2]>>>24-(c+2)%4*8&255,f=0;f<4&&c+.75*f<r;f++)i.push(n.charAt(o>>>6*(3-f)&63));var a=n.charAt(64);if(a)for(;i.length%4;)i.push(a);return i.join("")},parse:function(e){var r=e.length,n=this._map,i=this._reverseMap;if(!i){i=this._reverseMap=[];for(var c=0;c<n.length;c++)i[n.charCodeAt(c)]=c}var o=n.charAt(64);if(o){var f=e.indexOf(o);-1!==f&&(r=f)}return function(e,r,n){for(var i=[],c=0,o=0;o<r;o++)if(o%4){var f=n[e.charCodeAt(o-1)]<<o%4*2,a=n[e.charCodeAt(o)]>>>6-o%4*2;i[c>>>2]|=(f|a)<<24-c%4*8,c++}return t.create(i,c)}(e,r,i)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}(),CryptoJS.lib.Cipher||function(e){var t=CryptoJS,r=t.lib,n=r.Base,i=r.WordArray,c=r.BufferedBlockAlgorithm,o=t.enc,f=(o.Utf8,o.Base64),a=t.algo.EvpKDF,s=r.Cipher=c.extend({cfg:n.extend(),createEncryptor:function(e,t){return this.create(this._ENC_XFORM_MODE,e,t)},createDecryptor:function(e,t){return this.create(this._DEC_XFORM_MODE,e,t)},init:function(e,t,r){this.cfg=this.cfg.extend(r),this._xformMode=e,this._key=t,this.reset()},reset:function(){c.reset.call(this),this._doReset()},process:function(e){return this._append(e),this._process()},finalize:function(e){return e&&this._append(e),this._doFinalize()},keySize:4,ivSize:4,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:function(){function e(e){return"string"==typeof e?_:v}return function(t){return{encrypt:function(r,n,i){return e(n).encrypt(t,r,n,i)},decrypt:function(r,n,i){return e(n).decrypt(t,r,n,i)}}}}()}),u=(r.StreamCipher=s.extend({_doFinalize:function(){return this._process(!0)},blockSize:1}),t.mode={}),d=r.BlockCipherMode=n.extend({createEncryptor:function(e,t){return this.Encryptor.create(e,t)},createDecryptor:function(e,t){return this.Decryptor.create(e,t)},init:function(e,t){this._cipher=e,this._iv=t}}),l=u.CBC=function(){var t=d.extend();function r(t,r,n){var i=this._iv;if(i){var c=i;this._iv=e}else c=this._prevBlock;for(var o=0;o<n;o++)t[r+o]^=c[o]}return t.Encryptor=t.extend({processBlock:function(e,t){var n=this._cipher,i=n.blockSize;r.call(this,e,t,i),n.encryptBlock(e,t),this._prevBlock=e.slice(t,t+i)}}),t.Decryptor=t.extend({processBlock:function(e,t){var n=this._cipher,i=n.blockSize,c=e.slice(t,t+i);n.decryptBlock(e,t),r.call(this,e,t,i),this._prevBlock=c}}),t}(),p=(t.pad={}).Pkcs7={pad:function(e,t){for(var r=4*t,n=r-e.sigBytes%r,c=n<<24|n<<16|n<<8|n,o=[],f=0;f<n;f+=4)o.push(c);var a=i.create(o,n);e.concat(a)},unpad:function(e){var t=255&e.words[e.sigBytes-1>>>2];e.sigBytes-=t}},h=(r.BlockCipher=s.extend({cfg:s.cfg.extend({mode:l,padding:p}),reset:function(){s.reset.call(this);var e=this.cfg,t=e.iv,r=e.mode;if(this._xformMode==this._ENC_XFORM_MODE)var n=r.createEncryptor;else{n=r.createDecryptor;this._minBufferSize=1}this._mode&&this._mode.__creator==n?this._mode.init(this,t&&t.words):(this._mode=n.call(r,this,t&&t.words),this._mode.__creator=n)},_doProcessBlock:function(e,t){this._mode.processBlock(e,t)},_doFinalize:function(){var e=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){e.pad(this._data,this.blockSize);var t=this._process(!0)}else{t=this._process(!0);e.unpad(t)}return t},blockSize:4}),r.CipherParams=n.extend({init:function(e){this.mixIn(e)},toString:function(e){return(e||this.formatter).stringify(this)}})),y=(t.format={}).OpenSSL={stringify:function(e){var t=e.ciphertext,r=e.salt;if(r)var n=i.create([1398893684,1701076831]).concat(r).concat(t);else n=t;return n.toString(f)},parse:function(e){var t=f.parse(e),r=t.words;if(1398893684==r[0]&&1701076831==r[1]){var n=i.create(r.slice(2,4));r.splice(0,4),t.sigBytes-=16}return h.create({ciphertext:t,salt:n})}},v=r.SerializableCipher=n.extend({cfg:n.extend({format:y}),encrypt:function(e,t,r,n){n=this.cfg.extend(n);var i=e.createEncryptor(r,n),c=i.finalize(t),o=i.cfg;return h.create({ciphertext:c,key:r,iv:o.iv,algorithm:e,mode:o.mode,padding:o.padding,blockSize:e.blockSize,formatter:n.format})},decrypt:function(e,t,r,n){return n=this.cfg.extend(n),t=this._parse(t,n.format),e.createDecryptor(r,n).finalize(t.ciphertext)},_parse:function(e,t){return"string"==typeof e?t.parse(e,this):e}}),b=(t.kdf={}).OpenSSL={execute:function(e,t,r,n){n||(n=i.random(8));var c=a.create({keySize:t+r}).compute(e,n),o=i.create(c.words.slice(t),4*r);return c.sigBytes=4*t,h.create({key:c,iv:o,salt:n})}},_=r.PasswordBasedCipher=v.extend({cfg:v.cfg.extend({kdf:b}),encrypt:function(e,t,r,n){var i=(n=this.cfg.extend(n)).kdf.execute(r,e.keySize,e.ivSize);n.iv=i.iv;var c=v.encrypt.call(this,e,t,i.key,n);return c.mixIn(i),c},decrypt:function(e,t,r,n){n=this.cfg.extend(n),t=this._parse(t,n.format);var i=n.kdf.execute(r,e.keySize,e.ivSize,t.salt);return n.iv=i.iv,v.decrypt.call(this,e,t,i.key,n)}})}(),CryptoJS.mode.ECB=function(){var e=CryptoJS.lib.BlockCipherMode.extend();return e.Encryptor=e.extend({processBlock:function(e,t){this._cipher.encryptBlock(e,t)}}),e.Decryptor=e.extend({processBlock:function(e,t){this._cipher.decryptBlock(e,t)}}),e}(),function(){var e=CryptoJS,t=e.lib.BlockCipher,r=e.algo,n=[],i=[],c=[],o=[],f=[],a=[],s=[],u=[],d=[],l=[];!function(){for(var e=[],t=0;t<256;t++)e[t]=t<128?t<<1:t<<1^283;var r=0,p=0;for(t=0;t<256;t++){var h=p^p<<1^p<<2^p<<3^p<<4;h=h>>>8^255&h^99,n[r]=h,i[h]=r;var y=e[r],v=e[y],b=e[v],_=257*e[h]^16843008*h;c[r]=_<<24|_>>>8,o[r]=_<<16|_>>>16,f[r]=_<<8|_>>>24,a[r]=_;_=16843009*b^65537*v^257*y^16843008*r;s[h]=_<<24|_>>>8,u[h]=_<<16|_>>>16,d[h]=_<<8|_>>>24,l[h]=_,r?(r=y^e[e[e[b^y]]],p^=e[e[p]]):r=p=1}}();var p=[0,1,2,4,8,16,32,64,128,27,54],h=r.AES=t.extend({_doReset:function(){if(!this._nRounds||this._keyPriorReset!==this._key){for(var e=this._keyPriorReset=this._key,t=e.words,r=e.sigBytes/4,i=4*((this._nRounds=r+6)+1),c=this._keySchedule=[],o=0;o<i;o++)if(o<r)c[o]=t[o];else{var f=c[o-1];o%r?r>6&&o%r==4&&(f=n[f>>>24]<<24|n[f>>>16&255]<<16|n[f>>>8&255]<<8|n[255&f]):(f=n[(f=f<<8|f>>>24)>>>24]<<24|n[f>>>16&255]<<16|n[f>>>8&255]<<8|n[255&f],f^=p[o/r|0]<<24),c[o]=c[o-r]^f}for(var a=this._invKeySchedule=[],h=0;h<i;h++){o=i-h;if(h%4)f=c[o];else f=c[o-4];a[h]=h<4||o<=4?f:s[n[f>>>24]]^u[n[f>>>16&255]]^d[n[f>>>8&255]]^l[n[255&f]]}}},encryptBlock:function(e,t){this._doCryptBlock(e,t,this._keySchedule,c,o,f,a,n)},decryptBlock:function(e,t){var r=e[t+1];e[t+1]=e[t+3],e[t+3]=r,this._doCryptBlock(e,t,this._invKeySchedule,s,u,d,l,i);r=e[t+1];e[t+1]=e[t+3],e[t+3]=r},_doCryptBlock:function(e,t,r,n,i,c,o,f){for(var a=this._nRounds,s=e[t]^r[0],u=e[t+1]^r[1],d=e[t+2]^r[2],l=e[t+3]^r[3],p=4,h=1;h<a;h++){var y=n[s>>>24]^i[u>>>16&255]^c[d>>>8&255]^o[255&l]^r[p++],v=n[u>>>24]^i[d>>>16&255]^c[l>>>8&255]^o[255&s]^r[p++],b=n[d>>>24]^i[l>>>16&255]^c[s>>>8&255]^o[255&u]^r[p++],_=n[l>>>24]^i[s>>>16&255]^c[u>>>8&255]^o[255&d]^r[p++];s=y,u=v,d=b,l=_}y=(f[s>>>24]<<24|f[u>>>16&255]<<16|f[d>>>8&255]<<8|f[255&l])^r[p++],v=(f[u>>>24]<<24|f[d>>>16&255]<<16|f[l>>>8&255]<<8|f[255&s])^r[p++],b=(f[d>>>24]<<24|f[l>>>16&255]<<16|f[s>>>8&255]<<8|f[255&u])^r[p++],_=(f[l>>>24]<<24|f[s>>>16&255]<<16|f[u>>>8&255]<<8|f[255&d])^r[p++];e[t]=y,e[t+1]=v,e[t+2]=b,e[t+3]=_},keySize:8});e.AES=t._createHelper(h)}();var a,i={};i.CryptoJS=CryptoJS,window._$jsvmprt=function(e,t,r){function n(e,t,r){return(n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(e){return!1}}()?Reflect.construct:function(e,t,r){var n=[null];n.push.apply(n,t);var i=new(Function.bind.apply(e,n));return r&&function(e,t){(Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}(i,r.prototype),i}).apply(null,arguments)}function i(e){return function(e){if(Array.isArray(e)){for(var t=0,r=new Array(e.length);t<e.length;t++)r[t]=e[t];return r}}(e)||function(e){if(Symbol.iterator in Object(e)||"[object Arguments]"===Object.prototype.toString.call(e))return Array.from(e)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance")}()}for(var c=[],o=0,f=[],a=0,s=function(e,t){var r=e[t++],n=e[t],i=parseInt(""+r+n,16);if(i>>7==0)return[1,i];if(i>>6==2){var c=parseInt(""+e[++t]+e[++t],16);return i&=63,[2,c=(i<<=8)+c]}if(i>>6==3){var o=parseInt(""+e[++t]+e[++t],16),f=parseInt(""+e[++t]+e[++t],16);return i&=63,[3,f=(i<<=16)+(o<<=8)+f]}},u=function(e,t){var r=parseInt(""+e[t]+e[t+1],16);return r>127?-256+r:r},d=function(e,t){var r=parseInt(""+e[t]+e[t+1]+e[t+2]+e[t+3],16);return r>32767?-65536+r:r},l=function(e,t){var r=parseInt(""+e[t]+e[t+1]+e[t+2]+e[t+3]+e[t+4]+e[t+5]+e[t+6]+e[t+7],16);return r>2147483647?0+r:r},p=function(e,t){return parseInt(""+e[t]+e[t+1],16)},h=function(e,t){return parseInt(""+e[t]+e[t+1]+e[t+2]+e[t+3],16)},y=y||this||window,v=(Object.keys,e.length,0),b="",_=v;_<v+16;_++){var g=""+e[_++]+e[_];g=parseInt(g,16),b+=String.fromCharCode(g)}if("HNOJ@?RC"!=b)throw new Error("error magic number "+b);v+=16,parseInt(""+e[v]+e[v+1],16),v+=8,o=0;for(var m=0;m<4;m++){var S=v+2*m,k=""+e[S++]+e[S],C=parseInt(k,16);o+=(3&C)<<2*m}v+=16,v+=8;var B=parseInt(""+e[v]+e[v+1]+e[v+2]+e[v+3]+e[v+4]+e[v+5]+e[v+6]+e[v+7],16),x=B,w=v+=8,z=h(e,v+=B);z[1],v+=4,c={p:[],q:[]};for(var E=0;E<z;E++){for(var O=s(e,v),I=v+=2*O[0],R=c.p.length,A=0;A<O[1];A++){var M=s(e,I);c.p.push(M[1]),I+=2*M[0]}v=I,c.q.push([R,c.p.length])}var D={5:1,6:1,70:1,22:1,23:1,37:1,73:1},P={72:1},q={74:1},F={11:1,12:1,24:1,26:1,27:1,31:1},j={10:1},J={2:1,29:1,30:1,20:1},H=[],$=[];function X(e,t,r){for(var n=t;n<t+r;){var i=p(e,n);H[n]=i,n+=2,P[i]?($[n]=u(e,n),n+=2):D[i]?($[n]=d(e,n),n+=4):q[i]?($[n]=l(e,n),n+=8):F[i]?($[n]=p(e,n),n+=2):(j[i]||J[i])&&($[n]=h(e,n),n+=4)}}return U(e,w,x/2,[],t,r);function N(e,t,r,s,l,v,b,_){null==v&&(v=this);var g,m,S,k=[],C=0;b&&(g=b);var B,x,w=t,z=w+2*r;if(!_)for(;w<z;){var E=parseInt(""+e[w]+e[w+1],16);w+=2;var O=3&(B=13*E%241);if(B>>=2,O>2)O=3&B,B>>=2,O<1?(O=B)<4?(g=k[C--],k[C]=k[C]-g):O<6?(g=k[C--],k[C]=k[C]===g):O<15&&(g=k[C],k[C]=k[C-1],k[C-1]=g):O<2?(O=B)<5&&(x=p(e,w),w+=2,g=l[x],k[++C]=g):O<3?(O=B)<6||(O<8?g=k[C--]:O<12&&(x=d(e,w),f[++a]=[[w+4,x-3],0,0],w+=2*x-2)):(O=B)<2?(g=k[C--],k[C]=k[C]<g):O<9&&(x=p(e,w),w+=2,k[C]=k[C][x]);else if(O>1)if(O=3&B,B>>=2,O>2)(O=B)>5?(x=p(e,w),w+=2,k[++C]=l["$"+x]):O>3&&(x=d(e,w),f[a][0]&&!f[a][2]?f[a][1]=[w+4,x-3]:f[a++]=[0,[w+4,x-3],0],w+=2*x-2);else if(O>1){if((O=B)>2)if(k[C--])w+=4;else{if((x=d(e,w))<0){_=1,X(e,t,2*r),w+=2*x-2;break}w+=2*x-2}else if(O>0){for(x=h(e,w),g="",A=c.q[x][0];A<c.q[x][1];A++)g+=String.fromCharCode(o^c.p[A]);k[++C]=g,w+=4}}else O>0?(O=B)>1?(g=k[C--],k[C]=k[C]+g):O>-1&&(k[++C]=y):(O=B)>9?(x=p(e,w),w+=2,g=k[C--],l[x]=g):O>7?(x=h(e,w),w+=4,m=C+1,k[C-=x-1]=x?k.slice(C,m):[]):O>0&&(g=k[C--],k[C]=k[C]>g);else if(O>0){if(O=3&B,B>>=2,O<1){if((O=B)>9);else if(O>5)x=p(e,w),w+=2,k[C-=x]=0===x?new k[C]:n(k[C],i(k.slice(C+1,C+x+1)));else if(O>3){x=d(e,w);try{if(f[a][2]=1,1==(g=N(e,w+4,x-3,[],l,v,null,0))[0])return g}catch(b){if(f[a]&&f[a][1]&&1==(g=N(e,f[a][1][0],f[a][1][1],[],l,v,b,0))[0])return g}finally{if(f[a]&&f[a][0]&&1==(g=N(e,f[a][0][0],f[a][0][1],[],l,v,null,0))[0])return g;f[a]=0,a--}w+=2*x-2}}else if(O<2){if((O=B)>12)k[++C]=u(e,w),w+=2;else if(O>8){for(x=h(e,w),O="",A=c.q[x][0];A<c.q[x][1];A++)O+=String.fromCharCode(o^c.p[A]);w+=4,k[C]=k[C][O]}}else if(O<3)(O=B)>11?(g=k[C],k[++C]=g):O>0&&(k[++C]=g);else if((O=B)<1)k[C]=!k[C];else if(O<3){if((x=d(e,w))<0){_=1,X(e,t,2*r),w+=2*x-2;break}w+=2*x-2}}else if(O=3&B,B>>=2,O>2)(O=B)<1&&(k[++C]=null);else if(O>1){if((O=B)<9){for(g=k[C--],x=h(e,w),O="",A=c.q[x][0];A<c.q[x][1];A++)O+=String.fromCharCode(o^c.p[A]);w+=4,k[C--][O]=g}}else if(O>0)(O=B)<4?(m=k[C--],(O=k[C]).x===N?O.y>=1?k[C]=U(e,O.c,O.l,[m],O.z,S,null,1):(k[C]=U(e,O.c,O.l,[m],O.z,S,null,0),O.y++):k[C]=O(m)):O<6&&(k[C-=1]=k[C][k[C+1]]);else{if((O=B)<1)return[1,k[C--]];O<14?(m=k[C--],S=k[C--],(O=k[C--]).x===N?O.y>=1?k[++C]=U(e,O.c,O.l,m,O.z,S,null,1):(k[++C]=U(e,O.c,O.l,m,O.z,S,null,0),O.y++):k[++C]=O.apply(S,m)):O<16&&(x=d(e,w),(I=function t(){var r=arguments;return t.y>0||t.y++,U(e,t.c,t.l,r,t.z,this,null,0)}).c=w+4,I.l=x-2,I.x=N,I.y=0,I.z=l,k[C]=I,w+=2*x-2)}}if(_)for(;w<z;)if(E=H[w],w+=2,O=3&(B=13*E%241),B>>=2,O<1)if(O=3&B,B>>=2,O>2)(O=B)<1&&(k[++C]=null);else if(O>1){if((O=B)<9){for(g=k[C--],x=$[w],O="",A=c.q[x][0];A<c.q[x][1];A++)O+=String.fromCharCode(o^c.p[A]);w+=4,k[C--][O]=g}}else if(O>0)(O=B)<4?(m=k[C--],(O=k[C]).x===N?O.y>=1?k[C]=U(e,O.c,O.l,[m],O.z,S,null,1):(k[C]=U(e,O.c,O.l,[m],O.z,S,null,0),O.y++):k[C]=O(m)):O<6&&(k[C-=1]=k[C][k[C+1]]);else{var I;if((O=B)>14)x=$[w],(I=function t(){var r=arguments;return t.y>0||t.y++,U(e,t.c,t.l,r,t.z,this,null,0)}).c=w+4,I.l=x-2,I.x=N,I.y=0,I.z=l,k[C]=I,w+=2*x-2;else if(O>12)m=k[C--],S=k[C--],(O=k[C--]).x===N?O.y>=1?k[++C]=U(e,O.c,O.l,m,O.z,S,null,1):(k[++C]=U(e,O.c,O.l,m,O.z,S,null,0),O.y++):k[++C]=O.apply(S,m);else if(O>-1)return[1,k[C--]]}else if(O<2)if(O=3&B,B>>=2,O>2)(O=B)<1?k[C]=!k[C]:O<3&&(w+=2*(x=$[w])-2);else if(O>1)(O=B)<2?k[++C]=g:O<13&&(g=k[C],k[++C]=g);else if(O>0)if((O=B)<10){for(x=$[w],O="",A=c.q[x][0];A<c.q[x][1];A++)O+=String.fromCharCode(o^c.p[A]);w+=4,k[C]=k[C][O]}else O<14&&(k[++C]=$[w],w+=2);else if((O=B)<5){x=$[w];try{if(f[a][2]=1,1==(g=N(e,w+4,x-3,[],l,v,null,0))[0])return g}catch(b){if(f[a]&&f[a][1]&&1==(g=N(e,f[a][1][0],f[a][1][1],[],l,v,b,0))[0])return g}finally{if(f[a]&&f[a][0]&&1==(g=N(e,f[a][0][0],f[a][0][1],[],l,v,null,0))[0])return g;f[a]=0,a--}w+=2*x-2}else O<7&&(x=$[w],w+=2,k[C-=x]=0===x?new k[C]:n(k[C],i(k.slice(C+1,C+x+1))));else if(O<3)if(O=3&B,B>>=2,O<1)(O=B)>9?(x=$[w],w+=2,g=k[C--],l[x]=g):O>7?(x=$[w],w+=4,m=C+1,k[C-=x-1]=x?k.slice(C,m):[]):O>0&&(g=k[C--],k[C]=k[C]>g);else if(O<2)(O=B)>1?(g=k[C--],k[C]=k[C]+g):O>-1&&(k[++C]=y);else if(O<3)if((O=B)<2){for(x=$[w],g="",A=c.q[x][0];A<c.q[x][1];A++)g+=String.fromCharCode(o^c.p[A]);k[++C]=g,w+=4}else O<4&&(k[C--]?w+=4:w+=2*(x=$[w])-2);else(O=B)>5?(x=$[w],w+=2,k[++C]=l["$"+x]):O>3&&(x=$[w],f[a][0]&&!f[a][2]?f[a][1]=[w+4,x-3]:f[a++]=[0,[w+4,x-3],0],w+=2*x-2);else O=3&B,B>>=2,O<1?(O=B)<4?(g=k[C--],k[C]=k[C]-g):O<6?(g=k[C--],k[C]=k[C]===g):O<15&&(g=k[C],k[C]=k[C-1],k[C-1]=g):O<2?(O=B)<5&&(x=$[w],w+=2,g=l[x],k[++C]=g):O<3?(O=B)>10?(x=$[w],f[++a]=[[w+4,x-3],0,0],w+=2*x-2):O>6&&(g=k[C--]):(O=B)<2?(g=k[C--],k[C]=k[C]<g):O<9&&(x=$[w],w+=2,k[C]=k[C][x]);return[0,null]}function U(e,t,r,n,i,c,o,f){var a,s;null==c&&(c=this),i&&!i.d&&(i.d=0,i.$0=i,i[1]={});var u={},d=u.d=i?i.d+1:0;for(u["$"+d]=u,s=0;s<d;s++)u[a="$"+s]=i[a];for(s=0,d=u.length=n.length;s<d;s++)u[s]=n[s];return f&&!H[t]&&X(e,t,2*r),H[t]?N(e,t,r,0,u,c,null,1)[1]:N(e,t,r,0,u,c,null,0)[1]}},a=[i,,"undefined"!=typeof sessionStorage?sessionStorage:void 0,"undefined"!=typeof console?console:void 0,"undefined"!=typeof document?document:void 0,"undefined"!=typeof navigator?navigator:void 0,"undefined"!=typeof screen?screen:void 0,"undefined"!=typeof Intl?Intl:void 0,"undefined"!=typeof Array?Array:void 0,"undefined"!=typeof Object?Object:void 0],window._$jsvmprt("484e4f4a403f524300332d0511788d78e08713dc000000000000080a1b000b001e00011f0002000025003d46000306001a271f0c1b000b03221e0002240200030a0001101c18010005001c1b000b02221e00042418000a00011022011700061c18010007001f010200051f020200061f030200071f040200002500121b010b011b010b03041b010b043e001f050200002501981b000b041e0008221e000924131e000a02000b0200001a020a0001101f061800220117000a1c131e000c1a001f07460003060006271f2c050157131e000c1a002202000d1d000e2202000f1d00102218041d00112218071e00121d00132218071e00141d00152218071e0016220117000a1c131e000c1a001e001522011700071c0200001d00172218071e00181d0019221b000b041e001a1d001b221b010b011b010b02041d001c221b000b051e001d1d001e221b000b061e001f1d0020221b000b061e00211d0022221b000b051e00231d0024221b000b051e00251d0026221b000b051e00271d0028221b000b051e00291d002a221b000b051e002b1d002c22180622011700071c0200004801191d002d2218071e002e1d002f221b000b07221e0030240a000010221e0031240a0000101e00321d00332218011d00342218021d00352213221e0036240200370a0001101e00381d003922131e003a1e003b1d003c1f081b010b05260a00001017000b180802003d1d003e1b000b051e003f17000a180818031d004018080007131e000c1a00001f0602000025007f131e000c1a00221b000b051e001d1d001e221b000b061e001f1d0020221b000b061e00211d0022221b000b051e00231d0024221b000b051e00251d0026221b000b051e00271d0028221b000b051e00291d002a221b000b051e002b1d002c221b000b07221e0030240a000010221e0031240a0000101e00321d0033001f070200002501520200411f060a00001f0702000025005d1800221e0042240a0000101f0618061e003b1f07180718013a1700251b000b0818011807294801281a01221e0043240200440a0001101806281f0616001c18071801391700141806221e004524480018010a0002101f061806001f080200002500731b020b0826180148100a0002101f061b010b001e00461e0047221e00482418060a0001101f071b010b001e0049221e004a2418001807131e000c1a002218071d004b221b010b001e004c1e004d1d004c221b010b001e004e1e004f1d00500a0003101f081808221e0042240a000010001f091b000b09221e00512418000a000110221e0052240200002500241800020053281b020b00180019281f061b020b07221e00542418060a0001101c000a0001101c1807221e0054240200550a0001101c1809261807221e0043240200560a00011018060a0002101f0a180a001f081b000b0118061d00571b000b0118071d00581b000b0118081d005900005a000852636861657e5b42046670637f1962746262787e7f42657e6370767431767465317770787d7475077674655865747c166674737061613c62746262787e7f3c637477746374630c747f6574634e7c7465797e750970767447746378776806727e7e7a7874057c706572790643747654696110624e674e6674734e78752c394d663a38065e737b7472650420282929037078750a65787a657e7a4e667473087061614e7f707c740f7574677872744e617d7065777e637c0435667875097574677872744e78750735637476787e7f06637476787e7f0535646274630f6163787e637865684e637476787e7f03357e62027e6208637477746363746307637477746374630c637e7e654e637477746374630d727e7e7a7874547f70737d74750e727e7e7a78744e747f70737d74750566787565790c62726374747f4e6678756579067974787679650d62726374747f4e797478767965087d707f76647076741073637e666274634e7d707f766470767408617d7065777e637c1073637e666274634e617d7065777e637c0b706161527e75745f707c740c73637e666274634e7f707c740a70616147746362787e7f0f73637e666274634e67746362787e7f067e7f5d787f740e73637e666274634e7e7f7d787f7408677463787768576109357d707f76647076740c7061614e7d707f76647076740e5570657445787c74577e637c70650f6374627e7d6774755e6165787e7f620865787c744b7e7f740d65787c746b7e7f744e7f707c740f78624e617076744e67786278737d740b777e7264624e62657065740a7c706572795c747578701a39757862617d70683c7c7e75742b3177647d7d62726374747f38077c7065727974620d78624e77647d7d62726374747f07797862657e6368067d747f7665790b797862657e63684e7d747f04202524281962747264637865684e677463787778727065787e7f4e7078750a767465537065657463680c737065657463684e787f777e12667473706161203f213a232123202127232908657e426563787f76047b7e787f012105627d78727403747f7204446577290561706362740350544207747f7263686165027867047c7e7574035253520361707505417a7262260761707575787f76047a74686207777e6354707279012c04616462790f78624e747f7263686165787e7f2c2001370f767465527e7c7c7e7f417063707c6211767465547062684378627a417063707c620d747f7263686165417063707c62",a);var c=a[1],s=c.getCommonParams,u=c.getEasyRiskParams,l=c.encryptParams;window.genXTTParams=function(e){return l(e)};"""
-
-
-def get_acrawler():
-    return """Function(function(t){return'w={S(S,K){if(!a[S]){a[S]={};for(y=0;y<S;y)a[S][S.charAt(y)]=y}a[S][K]}K=String.fromCharCode,a={},y={x:(K){null==K?"":""==K?null:y.y(K,32,(a){S("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",K.charAt(a})},y:(S,a,y){p,m,o,T,l,r,k,i=[],J=4,q=4,j=3,I="",b=[],z={val:y(0),position:a,index:1};for(p=0;p<3;p+=1)i[p]=p;for(o=0,l=Math.pow(2,2),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;switch(o){case 0:for(o=0,l=Math.pow(2,8),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;k=K(o)1:for(o=0,l=Math.pow(2,16),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;k=K(o)2:""}for(i[3]=k,m=k,b.push(k);;){if(z.index>S)"";for(o=0,l=Math.pow(2,j),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;switch(k=o){case 0:for(o=0,l=Math.pow(2,8),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;i[q]=K(o),k=q-1,J--1:for(o=0,l=Math.pow(2,16),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;i[q]=K(o),k=q-1,J--2:b.join("")}if(0==J&&(J=Math.pow(2,j),j),i[k])I=i[k]if(k!==q)null;I=m+m.charAt(0)}b.push(I),i[q]=m+I.charAt(0),m=I,0==--J&&(J=Math.pow(2,j),j)}}};y};""==typeof define&&define.amd?define({w}):"undefined"!=typeof module&&null!=module?module.exports=w:"undefined"!=typeof angular&&null!=angular&&angular.module("w",[]).factory("w",{w}),eval(w.x("G4QwTgBA+gLgngBwKYHsBmBeARGgrgOwGMYBLFfLDDeZdCAZTgFsAjFAGwDJOsBnZtu0rVEqNAwEcAdCRhIwIGCjAB+PEVLkAFGgCUAbzBIYuMPgg0xENAF8AXOuJl8Og0ZNnr3HASflhlnSMrBzcaFKE5LwwYLjEylQYwYJhAIRUydIIYChKlip8kkJ2geK2ANwAKgCCAMIYjpouBo3O1joANCAdLB0AJh2EHWAG+Ljs7FRg3Fpg1AAWJLy65aCQ+B0kHSgY+jYdkyhSfRiEKoTHANQAjHYADOVoylooANpYACRYl+wAuhgoTYYB4kAA87HKJEul10b3w2C+lxI/0Ir3wv0ezxIwIOAKk7CQ+AA5jB5hg+vjCST5pDwZDobDXsjyUyMe5TOYkJ1ur1ASNXtdfjZWuQIFywNsDh0YB1gB04C1fE0IPNXPp6K9odV/rYReYANZaNzGDkMV7VAC0FqFeogTE6SBazzWEBAGF6JywWEGwPKhFB4QJxNJfoZ+hdc3ChHm4FqKD6SGqMC0hBWfUuSRiJGJUjQOSYtRjYDjCa0IAAeiMuhgy6DQdddJdCJckDdLmWAHwdhucABMAFZ+zZ2Z4+jYxhMqMAZsAFksVi6iR1ah0AB4dACSHXoGFevw61V9cBmRIwCsxYC0LoA7gDLr2AFQQlCg6/lAwugBeGGuAGYHyQszbLoACkvYACzXJCaAvBmvYdHcVBaL+nCfrougkDBiE1ihWifl2GC9uhBiYVo2F4QRRG6CO+ACtu5pWr8GKkb2VBoSg2QgPgJwuBKKC6NscEPhxCjca8dz7huAKcWJgr0Vq/yXBu5RIOwvBIBApHgWxuinhqlrWvR2pJOavwPkSKlqRppEAGw6XpDGGfp/zOekFmqepmkwX+On1PpjFriZBn7loUn+dauhSKuiRICoGoKQ0QEblICBDMlQbLpuUifmuuh2PFlzGclIAIAg7BwFo661CsHlIPopHXP26RoSwRggPq5QiVxPFAfxm7SaJfQCvuzkNEqzhlj0H7gBAJy2ly02QG64BErgTCEjAvDlDR7QSkgKVDPtGXdPtOWkvONjbSao4HRg3QUkG7r9FFGBIM934ymOsE2ZuFrgQJKBCRuFq9jYNi1V5WjXEhKFoRhMG/kh+EdoR6EOVa2pGf8RJaPpNy/DVVmQ/2On+T+Lmma8eOChiEOkQA7KTpkYFazmWep9UwQAnM1uitUg7XlA5wVYyItDiES4NEyxMOoehtlI5R6GjbguOmYTnmkQAHPZQUBV13EYLxwGCYRwkyUNElGYxrz2iArwG0NNPbBbw26Nj7N1Q1dy85zUOsRgaGkjk15msF5T8+1NijQAfs5Uua1hiso1RBXGROEJ0zBAdocLAWjc5KPudL3O64aAn1OX0rif8NmDlzXMPjANeXM3RK/BERYlomybVV2HYPFnUPQ4Huhp/8wAoCQfQQIPVl+3+vORx1edOczzncJLQ8j8hcvw2RssUSnxF+9pNbI6jBiO0bvUCVJjvDeUMRwH7Q2EL8ry9v81wdDvp7ioJH6wNwLSllOhGu1FrrmEloQRQ0YtAKlfq8d+3A34f0FNwP+r0gJoOGjXfoyD0FENAXKBUugIE7UlmgbMIAJgv1Img1BhCa6YKQv/HBzCJL4NwVwuSMpgDgIkpAjw0DyhoJxIQK0NhAZm2BqDIedlR7X2Nn1GRj4H5W3vq7OSZMNz/AQFoLA644DeiwDtfASBQ6rleHAX4hjCpgAUBVDcNxIoACsp4uG9NY6EtisCRV4LgFg0RLwNkuP4/xuMDwa2sjBHWo9V4jXXqZTgxdE5Qx9qPZeCdYlQ1lnDUi5EL6p2ZqkNmQ9Gajz8o5fcp4EwEjkGHG2tRaYly0FzHSyjb6m3Ua7K2BdKZ2wdtopiLtBpu1aRzBq1wl5tRXnrNexlnJ1i3m0gOu8CneWTpfceGA0m5MRgkhZSSlmmVBHsz2kNrjYVznrQiH9qZMSCvefcBlLkNRzrpIKSSEr7IXuBWZAt5nhRORTbUAAfcFqz0lFKVmPUp5S1mdItjfPiAMhKhQGt1N2IN3kwTPrckFotnIAHp9mQ0UYSmpxLTLpAubVBev5AWC12cAHJkN1mw3lknHCnAj6X33jvYpaNjk0u1B2cl2tkWDVRSbNR5txmaKxbJfc9Rqg3GthgYGgotVxSkLwdgJBCBcmqMuPKe48UZOld1WVqihIaMksqp2tTRXNz0fS+eHzmXAupQM8VEr2UNXydyg+584W7K2kPT5iSxW/EuJK/FusiV+v+B69J1wR7sRRSo9FZsHX9Qfjo14GN9GGOMaYnaq4IDfCGXozcjYsAQBMboNWmNHkxMZd6oWes5LVLbZqQq2N5KDowLUQN2dg2Cu2VRLpaKekKuxf0gdxkhmFtGWumJkNDloRBSoGRNk7AvGzd0+VmKN0g03aREmRzk3JO1CBBNWhKlUpFim7M0QuLGroH8hqAKslzPKLs1I1xx1aDPvymdx651aPGXJHhvxuCpFwV/FQuCdUO0uEAv6vw7Bv2hP8cSHQMNYfAvuK28q5GWspbCKDcr7V9MdWu4dxlm6fG+Hoz2fstZdpjSmqFoHyK4WFSGoVcLZ10bzQxgtIzmOKVrZJS9CMk2+rvb8C0j7r0vvzqpxIj7rifL7QFaKRLMUgsitFKgsVdkwQlMlVKWUMormSjlVceVdnFVKuVSqZrOO/p498sVFoi2Wh1dC3JO8uX73WcJ1Ueo1QuiQDiN0gYqQhh2nFxaEAWCvVBCACO3BYuvFbJcAmHRVQsF0GDVtBNFNgetYbHN8780wexXJU8bqfkjsfdR0a0JEVTLif529pztTZggGFjlk6r3TvQpa59XyQWWo6TelTI3fjlh/d5GZ/6gWdVo31FrKqCHQnfruYjv1SMIQQvuCjINQOiePgt1bYLfigkffErNMrGvyua064asbXgce3jc+FCVUhzwG/7Ibz3vn90lsOKBhGM5ChdG6PcPQcQnHR4QHEkZxraDQB0R04YZpui9JcNARX8MU6K2yKBEAEDgHUhufAyZujXBspVjo8I4uE+J6jhoVOSsY8p0gfcJxGdgGZ6zwx3wQCXF6BzlYDCuyVLuMI00fQuw2XSHcGYJwLQ2XAuCmyKE+gCTon0ZiMEtcdkUUREnkAceS+l8mcnlPoRi5p57/cSuroiNnukZCvZODm9QyzX8vZwW/muKH3DnAMAx/6HWDAWsfTpiGJ/QYQoVd26oL+TLcwXdIBZ2774HuWxxor2L3+nPucYGL6X2X3vK8t5r37namTg+h90OHi0kfo+x76PHxPP8+gp458MFPaf4TpjAJcDYrxfzc6FPsbEvOieZeS0L/c2XRd04D43mX5P5eK85/sHYG/+ek+wOXnfNOxf+9NG6I/bPa8CXoFmHMeYUAFi7vGLkEALnSYK/LoJ0S8F0bLL0foX0cfPLdMGEFgDMFAB0ASVsQiJ/TwFgfYGAMaDQCaPnKafQJ4CAmaE4B4cfFgMRMMF0HHFwPnGqISVECSVYGaOYdgB0QYN2coEAFKXAXgVUKsdAx8VEQUMGDoXgOcXgDoBADAXgKQAAMXxw2FwAwAAHkWA3EkBiApB9QkA4BeBIVedMtEs9hqwHgSCrwZoWBNJzA9AkBXh5cSt3RMDORKRgwyQTpLp0tlCtBicjpUsaRLCXRyCfRoDsQ+haR0wOd3xmwAQ/DNgBIoQMDSIsAAAJAAOTUIACkAABFQAAJVqCwHSFTGDhQFDgsVDgAFFHFngsB5AchIAmAQAiQjUIAxhWB5Bq0mwVgXA3p0wfxOdqDU8McLDnQZpJgHh2BQRwJyh2AaCZpJDLwBj7wnwVgkDeVlg6xHx2AbBBiYiXRZD+j+gMwlcOhVCEB/tZxBjHwriWDBirjBRWDIA4Ab4BiVg4BH5biHwvirY0d9xkwBj5Q3YOgYI3o5RcAiMCYqsBIahahDFwItZwIkBwI0BwIQBwI7hfw0B+wIJfw7g7gII7gbJrk8S0AtZ6YbJCBCA7gkBrhUSkBCTmSWTWSWTaSuZ6ZCAbIiTCSBwtYSS+hwJwJewM0Y8mTrhwICTewiTfwkBexRT6Y7ghThTIJCBRS/wGTsSpTeThS7hrglT0TVTrgBSxStTrk1SOcBTwJ6ZhSNTxS7h+wdSZTTd5TFS7hMTjTCBNSmT+wtT6YDSOdVTIIHS8SeSZTrkQB9STT9SPSiTrlrhvTSTUTjSBSiSIyDSSTgzEzNS0BCSYyM00yfS7gtZrgoz9S+g7guZewlS1S+TSTrkZSiT+xaTwIWAbIWBeSQABw7hCBsyHSWAtTaSEz8y8zwzlSeyVS7SzSmTUTpS7g9TRy7glSZS0B+THTJSQyGTrl4zGz9Myz0zRT6TI8tz6SsSozrk1yMz6ZfxbSvTeTmyM1sy0yFTty8TzzmS8zRTXT8zxTMz9NryTTsz1SZyDTnSFysTXylTPThScy/yAz6TrSlyBTRTqy/TNzYK/yQADz0ztyWBiTyy7g8z9S8yuZ4z1T+xfwYKRTCzCTwKpS3ToylSuZgKYyiTXTMy7y9Smy+TaKFzpTrg+g5TAzfxnz8zoLgz4z6SeSjdVT4ziK2KlyoyFTmzrSWBYLTSmKSzJLXzrg8zGylzSSiy8yAyBSAz+xCTfwuYDTew+zUySSCyfSFTXz5STynLnKGSIICLPzHL5S8SoyLyiLrhqzSyxyYz6Zex6STKOd0KBSeSSK7hOyFSbJ1zjVuKFy9KST7SpSYzXSVKM0AyILYLvTI8jzGpvLCSvyazILAKuLLSHTSz5TGryqjzexqyQy4zLz0zbT0ytKpy1T7StT1KY84q5T8rAz1KvTiz8Kyr3y4zKqXLexuyarOzAq0BGpLLuzrzlTgLBr5S+yRqSSxrAKIIprxTbKXLyqPzKr0yhTeqCqdqvS9rIqcrRr6SlTZK7TmTrhOzrlOyUqeTyydyCrexPqaKnL0TDq8qTrWLzqry3yKqiLZSFz9SPrgyeL9MlzaTewY91zOTMKGTI8cLMrSSGqXLfw3K4rZTTySycKVKmyey7LYKkrEq+SKzfwLL2bAbsSgqSrCaIqjzfwtZiScbrKqblLea/woLtKzr+bpbqK4L+bCbsLmKxLeTBKOafrYzOyIrubYyDSY9+yoKFTYbCbBzxTfx1TrL+w4qpTqzAKir6rCa+gfSxrrbbarLMyWL7L1ahLObMrKyca1y8T+q9qkL6KarAyUzirizGrexmq8TWr2qfSkKGTrTIzYyvzrScamSEqmT47+L1y65mbtasq9bgaDSBT+xZKiTrTTzbKRT9TxqAyMKnb9q06IKU6FynSEzm6PTgLa6FzSaRS+6FbsbrSAzRLnKtKx7B6kS/wR6HaB6C7wJrKp657sKp6qL8ymzMqs6Fz8KbbGoUynLvSkBrTvS0bVSeK56XaGqaKbyjSCamThajzrS+gIJT7Oqgqwq4rFrlq57aS1qNrsSZqmLW7LS/7WqAGV6mTgHObfxByr6vrfzpbvaY7xTrT4alSma26sGO6LLpan68GFz1rz7HT9TyH0KIrkTgyeTgysT6HJK/TcGu60SCHcKqH9NrqiLRThLkHG7mSVyq6ZSC7KLB6nSJHCGq7wyq7hHHSUKq6yKxHlKq6kqq6gHEqSaJHA6q786q68yoyY9oqzKSzsTMrSyBThaLTAz0HIJSLwqYzrLrLDHNSBTsLrKBGMabI65cqzHjHaarHRSMqAzJrYKHG4qnHgrMrN6hqYyPHMywbvHTdpr/GLGYzrGQnDbjSImqyXKqbP6/y+TImHyDyZS1rpbo626tGBTnGO6UrEn0bvqjcXbd71rimjrUrvata/rlzQb5qEyaH+z6mR76m5q+Hv71rf6SSHzJzsyWbOzfa4r4rBK4rQnS7hby7UaNHVSmG5KUrQ6Azpnky4rSzMGSTrLyGbIQBjrMyqnFarnBy/KQH+H+6faIy+hlnHTqrjTzmOz8mST1TIK4rA7mSJb1q+bHmAWbJ5TgXBTJnEzlyenWaAaBnUb47hmrmsSALAyGGHm4qrytK7H8XlzUa3mNK2bVmSSVn1rGyzL9MbaqyjTkX/r+mgb8zK6qzO6ByAWBa4WbyEXjmO7bTLq5rCL4byH6Z3yfSdyC6qTB76YAyJaeLwX/6RazL3HM6XmtqmKKbTb276TE6EaeGJWzLpWimFXZHlylXeSVXeSJWlr1XlzNXLztXm6gzZaDW67RXEb976YWLvq2nMa5XuzGWAzn7ZWzKWb9TlKSapb3TaGMHPK3TMywm27IrhXL7rTunzr6TJXvSTG+noGnX6ZKz4HLLGKCqIGcyzK1X+W4GtWEHK3AzHaa2sqyrKbSXgGvHA3Ry/wFXg7amnTIJfrEq4qz6rGyX+qWXx2O60yh7HbGHyWRSjmZmb6FHBTn7EreWTaFWCTgHjmAXSynXhbBWkXa236SyTzB7kSz3LGi253pWrG53ubXXcmBbH3P7n3yHKSz3sLamb3hb73uzQqtHR2WBZ2BTBzPmzGYzFnSWSLJSb20Lj2ILSmwrwPZ2f2VbuHjTMPAW53sKwqbIBS1zYKLK/STKBm8yznPKJ3O7cn7atK6rEWgbXH/a8zByoPwqqLOWQAn69m9TBOsTGWuZJr4m4Pom8zNGEqXaOO6SYzELezPLKKVaOaxL1TL2tZ8LIIBSG3XWEKxKP6tOgWYzNO52dPFOEWf2y2tXvS9PewXa8zpLwIjLlOf387inMqlP6OtZ1qW6xKfO52vzin47vPFOzOO6yLZmgPwu9OYzjOovuGHzJmwvnOMmj3TO9OEXaTTLt2nOtGOz0T1qgnL3rKnXqyz3ST8PqTcqqWtZ/322ou3L/abasTCTwW+3pGqyRS2uUTn7bKoujXyGuYUqTW43zr6POSAXRvfXMrzLkKNqM0HHCGYmNqAnByLL7HlOKOA6EvvPB6uZQP96G49LI2qyiOM1DvBz1rRHamC3+2C6uYnOkOnuoqNqnupmOdcq63B7yzy3QH3W8X7Omv5SYH3GM7XWEGkHeyNPwuSPeGKWUri3dOkaAfEHMzWHn30uEfPmke62TSrv0eYeseqz4eoOT6oHPLlrCfynG3LKwGq3gefuUe/u6eoeGegfWKqewenWbmEXofMexKiOFOEf6ZaGWfqev3+3if3XRKpqefHXdOkPZfAL5eY7FeafSyLLVfwHmfNe+ePyOfAfAL9fQele/udf6fQGhf7KbvReZaKWCulOP3efleOvrfEH5bueFOrHsWJLr7mnSzwI7LwqCfUPdeq31fIGH23fCfIegroeueFfY+LeC6QBX3E/Of3Sx6Iv3HI6A+NffeeuyXqLvHSP5ew+UfCfM/1qk+1effXelfCejes+TfwHo+22m+teNrI+RLG/w+ZfPeZrMzc/4uBS+zcLC+Y+lP1LS+mn8yEetZZKq+pf3eBfNqoKF6U/u/pf2e2+qKt/O+Qfd/lfa+3XFTt+i/T/Cf93Petrd/B/W+6/N/L/j/V+4/teN/sTLvTLAzx+ypOUv7yzKB98yHHQMhZWW6E9Vu61a2hlXWrYVdu61Ljj9TO4g9kea/P7tBWH628NKUnHMkv2HaS83ef3F1gf0Z64sfemA0genzIrE88ByvUKoQKrr2UaBafdxgFVwE4NnygZCFhzispiV2B4PD0qtWH7J8i+wgvnpo2N4Y8eBqZMsugI5yO9leN3HMuB3KpbMZuY3eaj/UEostJSbLfMpw1qY6DxWZ3RCjWwNrYNl2Vg+ziaRxLG1c+5zNEu62IYPNBu5DEAIl0Na+tfu6ffThQMaZgD9SpZewY/W94K8rBWsRwZf316CV/BWDH/nINAEoNQhVgm5im2n5WDPO2JfzqkMEZN0+2VrfjuuRdp2lSSY5VzouxmbEMay83aah3V+pjkNyLHHlpe0HJOt8KkzVmglR5J5cAarzYdouWmo7suhd/N9qSwLrqUu6LAMMo1GxqFV3Ki1EUiayWq984ywTAutMJFK0d6S2FcspdR066C1yoHe9h03bIZdYuClelkWRg4sBJ6GTWZpRVbpa0ZSv1RSmqWqEMMZmPwlhvM15KssOhwtEdoCK7Z2dNSCzMxmRSVKEkXaPPKDsnVMbANaSiHeRiwFDZQdmWRZZ5kK0IYU0GhppA2p3yLKRMoBbVKToEyRZbcKRhbe9gt0jxcxD+21MflSJvKeMCh/TUIbTWiryd7297U3CrWtKek2KGTWYSP3IYsAsu+ZZ9u7SIprkSuMo2mvAJiF+lGWaABUaKNlGNRrkmtdUZqNJHKidRqoxqKRwVFxksSeZeESWUc7pMiuMwl8mV2S52tJh97KPmJVfpNCP6kEf2siPtZKjjKMZaYX5WfYUtKGA4ONmSOpqEiHKFlSPK/xRpe0xKEVArnlyrasNzmLARUQt30ylkLy6TZ9rcJmYu0eS9zTMdmIPLLdaa8nMWmT1Goq1ewabMUbUw+5b1omjojutjVR70CtWLYuMb2Ff4U0hxBJZUk41tEOM64opSdoh2UYT8GWiVNWpKQn4KkFKiAweoQEjwvCxKopCjprSJH4iCRjlOMXuPjJbchxuYrUfqRjwxk/SApGPP2MVE5V2xspXMTHj7bqNYeEYxNqj3DFqj+KKtWwUWS24JDbxLJdAeuLxKMtvRP4lTpGNjFN0XG57JUQGUQnwCEqsTN0otRa6xjRKP3fCuQITEEk9uApOTpZU8a1M7yBgxrthRq4f106rjCHviL/HxjMqm3LGpFzMq8cbypPbkd6XJGllEJ5wyLhoMa68TomGg/Cm50a73taxIPLjrGSjKkk56E5VdrJPHHKcjcfFXoQWOQlUTLxKEgwZJIk6Fi6xSE+SRnSUkr0VJmVWsR/SiqjiiuNkPLt9XWq6NeyKVKCZXxxERi8RCEmMYSUWbpNzqHlVyp/QsqklKKqPFKvhLPYu0RJ+3WiUtQQEekJOnNf3i2IpZIDZSzEgcaxMcbSdIuCDTxpbUXFzjyRdTXJmJPimGjvS1lCSaDWMlUjZJknTSopMbJkUsSH9Vdve3VL2SkykEfSZSNrH6lqpzJQKfVKknNiyevompgpIcpEkOp+gtct1L0kQ9Epi01xo5OtEuStSBdQgBFSPogBKJFrPaShXzLKVLqnbD3gZ2XJq0wWhlUUbiOpGLcaxU0m8kKJiphiAWXYp4QexSmMTJ2XE4KkIKiYdjyGhAJEWYzWoOTj24nAGWmS4mNiQZLYzMRKKg6mdGWw5RiUdR1KmjYOjExwWgBHGdcSxpHEUYxNpIwdf+fdVhiDyQGUyNRRM4qSx0iYoymhaMxKhjOy6pTeyolIiYy1NLYzTcPowmfzMnapTRZHvAklaKtIelKe1YpuvSK4m1MJeYMifkSVR7f9cxZ7eSfTOVKojmRjAnMi2X1lKiWAltaWrTMk4my+gjMy2pyzG6o8kBMQjUblJ14EjuONtHEgqP5nekWSaXZTqzLJ6oyAWUosKaRzEl+kcaIsn2fqQFmRzeZMcgcWLPxk7ccZjUSWXjMFk7dJZFopGtaOFoFdSypYzFhP1Ua9lByF5G0hlyLljiCu1lDskgFO4IiyeIte6V1UQ770aS/JAcKH3LGOV8xYVeTgtwSpCSnptYtMYbOyEg8ZJys0UqdTYrfSNZGXBUkXL/Y8ypS23eSWxMTHi8SS2IrRlAPSnUsKWbE4kpy37AqlrZ2pbZo6Tt4bU9SnLNsqfI2qSMn5C8z2VmVRqCD75FlHqgbVbafzJ+BtU6i/Isp3UbKxLT+aiVvktkwFRFY+SAG6YkcOc4nFBaWyR6ONNWpMmyhS1nkNioFDlIsUgL0puyjqh1Ewb2WXmld1JBEjObHIcl0lS5vZcuYQBdpVyAyKC38Cv2fb8SnGyslGpguIUbVNRFlDcRQqXlfsbRq8ixgL0JkbVTcFNbkZQp+k0K158DXOSWKYW/NOxrC9hfqWrkI8mSeCj7mF1qYCLOymUjahFTr76YPZfJS9qoukW0K5Foi3mUosXmOLqFzi9RY23kXhShxyiyRZrOcq+KoemclsjuT5KIS+JSojceZ3kFF8+JhARmRqPtk2LkZRbdSXl1iU3MfRLs0RZTIRlwi6uRYviQUrdmUyDZJS0kgrXOawJ1xPnH1j6J3IVM8xBizhdoufplz1x+iyUpwrLLMKGle0xUQ/18EtLrpvDdpf0rD6pjUFCYWipPIAkK0g5AZD+n6VRlNDIWUHeSUpNHIM0SJqlJaiqWo4OV+5Js+dpWIaF8iQGNkx6T5P+lUi/wD/aeaDI9JzyMSX0rxVIvtrqSexkw7eQ/NwkFSypdFW+TZAPmxKkODsnhWnPPl7zL5FLWJaJVgX2VYlj8vec/NR6xK35mKj+WnKNzfzCFsS/+QKzh5iL/eJ5NFRtQgURViVG1GBRfND6gr0St8pBbBRQUdkOVpZDBajzy73tOyOCiKhS1rE3MCFyYqaZt1IWv9FFnizsd4t+X1zXFycmpV0tPI9K9pfSwxWc1hXwCLx7jARViQpb8rr5bi8RXKvBkKrQlNk5VTKsCUWr1ZPy61aRVtUjitFZY3Rb0srHaq6SndPsemWA4WK/Vh8vprYpxoSLvl3Y51Vyw0Vmr7VasqhU6pkUurY1ZCxRURITVOLFVNq1NUTLjl+zmStI0cTNKEoJLYeKfOTiktSXpKBxEqtRTpNIl5LTR3sypcqWKWjiix97OThUrr5VLLKNSwZTovBmsKlOPgq6jqMmWnDvVnSj1cOq9UcLvudSz1SMrg7NKJ1qXaZdXN4W5cNRSAOQdkxMV8KZZRYl2ustKUOVnmmyyUdssSryTCqHa6MiRKLUZp+w9MKZlSMLl2iWAfQJBWKKKl8C61tNM9UHOTIRir16M8UY4xKUtzyukFduUjRIpbCSJDLdxsyqbp/hm2qtFPvpmhoz0feL6zaqP0XFPTaKBG7Eu9VulyUb6MrDpoQDOkxtDy25K6al0ArEtIW53D+qUwukdspFk6piqKTEp8UVOrzFmWBITVCUwpSojOquSk0LD5RaSnDni3A6iM3O3I6TQmSUkAi/q881TQWU2FRLSFd3HakXUokvkMNfdNjYvNw0PUrZ3kv8eRs4p3SRp2tM7vO227Oa/qrm2MttwzQaDNmBgj+tVxc3VlVNTZWNh5q0bekNBIWxynT2CYo1wtmNcLfsulIkTIJo1L6eZu2qWaXy1mo2SD1/EvMKNom76hoMVE2N9KLLV8n5oakaD5SQWzzTFqLJxaEyGgqLcFoalEiWtYK8LWFvjJgrENcLPoB5MsablB6fQVdm5werfjHKLQtps9ISrDTYJ/qyPDEKQacCvl8qpNX8oF5mi3ZuoiNVtqjXJqY1jbPba/xiEeLM1Vqk7f8sT7naiJNHLBaDwn6Qzg+JE4WmGpRVtKYxAw76W9oWE4lC1r/DcS/wDl5c/tjigHTbTQDY07VYO0DhDpIGvapF2vQmRuoaFFd2GdmscWhM9p7yQAxA67dtqVXwNztE6+xYVvMWeMfqatYJU4123yiyFl25QWDReajzntNAlHZrKXFtr9t0cjHYW0h2djodG44ikSRB3fbM64O57VTqrL47adbA/7ajtNGw6JdCiqXa636E6USJIVX7eNr11C79l3XH9RGNbrjaJRzfedtDMQ1Ws+gFFUsj1zg19DblQu+5TlKZFCikaJdbTaFqh1SKR56fTOslNLLKa/dIugPUtyD1dVtudU7Wp1ocUR6+VpigiclJonx7w94MyGQyOj3e68pkTEOR0OlHf9w1UTM8eI0F1ELE9WeyPYQ1z370+gTI/zj80wWF6tlPyoFfeIjm5TsZONP8Qwp+3TMVF2e9bvXoN04kbKrwgvezNDnSispsqzGerqg7EzpdoHNzgt12GY6sxpkmljWXG3rKL6YY+hblIUpt7r1vG0UX6TFWptsyjLAaR5MXGRSuJC440nfoGXDCYJlFPdcstv1clWdt+iFWPsQ2KNlSflaPqCO01r7z1cchwUaugM6iBdm+oOXHMTkLkzFVelA8LLQMCjPhBdRDbOOVKDsqylEmrk82y6D16SAchrVo11p+MGaYbUzfmWeYWUYhAEj/XRWE6/DmGejH3duxFLUH8KzkmbthNT0/jhDkmlA5qxk2JbjmFBnOlQLw7x7JJEZa3f1sIGDqkeFBimu9QtKKHtNRkqcfdOCaItYR+m/UqiNIXlzUS65LbhAbHYkl5JFZPTk6U4OcHKKxLP5kgw9bdKHD0LIFhQam2bDJa5zJAAGU4XMLQjfhp1kgECPgsTD+yj8efXXIgBJq6HLMq4ZqF37NyXdDspAOYX/NL2Tc6I+f1kMAUiW+rPTmEcagRHXeendqgEaq5ITSSCKz6hkb+HCjeDOnWrfHuF0jcRDPQqKeIZ9HgCoZ/omQ0hPkauUKNZfZproeNVaHXmbOnigGTaa2UKDmIoiiKNFIYdtacVMg3STxIOalSabCMsR2jl6H7DuR8hkgCjmvMmx2xhyiXIuP4V1SWzCblqXaW4VRSMHJACP2+ORVXykTH4+Z2+N6dEu3xjmq+VBN0kU20JpAPDWhMh8oTVLHxsiZg5VVB6t3cKQvw53Ps76v2llr2SMH60fj1ohCmCaYqRNOjD62suoJc2LKMqJ/Qk/VrRZUskALvQk2uTeM/GKxGg9iqyZg4h9HGGg5KgKYjnCmXNkebkxm3pYaDUW7LNk3ev2VuSryjLfsOJzJqXsiZvGqrq7qaOwTypOk7/jbxvIkilRbnASdE2yFQD/9r+pcrWNrGgbQJwtPVW1XsUCsYxwteycXIxLhV9l+jBBQOG4W36HpQcjNGAYbF4sgJBaxwY1xxaWqpFtfMLgg0o7mn1JpJOuM5P2X71cSQZtnfJzc5TbhNkZ0qU3UjV+M1q14yJZZTXJZTsSqZ7kQczjaZnFKJFEcQXTQAjamW5HUM1XvDNBmvdWsK+aaXp3i0tWbum2QjJH6JKY+5lSmVmKJllrbNfZ4s0/K8llmjtdXfiXez/W0k1d6um2QbJdp8t7K5lHtb33sXa8e1yZgtV50LYRTFZcE/NTuVjNh9jTmtG4VlQK4ZmsxEK1s3pTfEdmDppjL5cx2Xq9mudy5c1tuQ6YUkuNtrIwz/Sr1SC4W9JAXm+v94ykhBALCTcMcbOJCFOPQuIzJRBlCtshkTbXVRqsZQGqabZozWgBQ4elyZDisC/eXm2FnoWy8k6WTPgv7931SO8I6ed6E4XsJCfdaiRao0QW8jSurU8cI7MyCgqPJD/Q1Ke0NViauTHlenR7PoTGpcdVTiArEq1jgOwVAFmgDku66ehgUkyR41MvmXlSd2zjrGWtHdkCu2FI3A6dHJKKOzB+ksvLJ3q1UKjRFFNuQ1dk/iehYXCtkRqHW01CKV4j46ZeY0VM36vdcbmFTUtscFtRpurhGYNozaiRoTQDRaefGNmxRzjGLg5OrZFiirS2oOZabIoFzaFQZQ6rCIAvAtGy65G8j4azH5NdROp7tlfzEPJW0Ddh82Vbw7mij76BVa0sQM1Md1tTYVpK2nRSsnC4JXOskm3MfJ5XZN4HTWlCLU3BUUa2038h8ZwpBHPzLkjmrXWuRdnxeYYss+AxY43q0AwVvbQteD0RWjjlG9Kt9VmshXEr0uoa8l3MFzalyI8rK5ExytDNTz56gSd/QLqN70DJV7USFUxPqlrhSEz9U9rrjNXPLsja5MBaJNbsOZfLPdmeyR3iNVm1yekROspumTajtNdVhnT+n8irrWjT5tchnk9WJybVhTqtzEVxlpZbbEvbTcVFazKbCZSdmFwzTi3DDno8+mKUk3f8lbtywhnGQsr8DSuS1wweNxP68tVhK1oefabL0ylEZhC58zWfWEtynFJVvymsZ+0pnMq3I6svXM+n/miZwjF9QOAE10MahgNdGpNsahvrmSFle5jtfDalllB7XBDgBcUYg0o52ZOllWxg5ax6SQm9m6KKZlXWTLfpKpt8Mko8k7G9QscjfQpVDq9j+tIRp5dUYZpQ2DJBhhkcLuTaS75anw6EYruEkWrRM9Rr5ojG52G7/tkms3YzEjN270dxc1nYoovVnjKUna68bRYZVwOM97Wl7y2bYHCT6ev6of1Xu7DPLujDNOstAXdLwzq1pa2Yx2HoGTm1yFG+fZwPy3SFxlOUlddEZX7S5x9oVvLaQ0LiL7S15yg/azp2Ki6Gpn+87psMpkn7Gyujg/UP311zOH9sVp+R6ug0NrXt0OzzSstMHj26IpAxg9zHbCoO2Dyhse2vv4PEbP9kDoyfg38C/bPtFLnWQY3A04L7XMLo2I6t2Un7BbeCQlQiozlBukTFivOQxVzGYHdXRCQMpOM8OMBwMgRyAOHvt0+H83PO/s2XZF3W5siv0m0KGqIPvRT9wIRC1ONmMPbuUqenctMY32rhg9dSugZRskV06V16UoyyYYysDhEFnq7jWGNiW22a10qsMbGuS0ZpXj69uKyLW9GzmJtDamINdaxL8LyVimn4MEnLCYnqw57VIYQ16Vs6dj9LYmXl52O8bt5Ox5qwCak0TrGVx4S/XUtUieSATB08E0gh7zqS4TFKe0OFa7swuVlHWXBzsfYV2bGPbjvLctp2PT1ZzWfktftkQ9un61Ack0MoUsBIZETyWsIt6fB9oNVIxrmQclLXJ8TTJfY0lWBFSK5nVDrMUyKuuN0wuzpYPm1fAjSNJSeNm0sc9jur1jn1dtssc/HqSk97KJY5//c5rJnE7hx5M6luqNXXBwQLvG6+qBex3+ws4xqNXfVNAvXnl8oF3AyWnTSlyghmJio6ifESH1lzAc5y3NmaGAmSneUjYz3lcwT6eZCZ5o8vYk3WnDls7ukK868dunqAwh3+Fee/hBnGGwnqy9GemSXaUL/sFOtI5hdxnjQyUdM6RGtKIRjupa9iyLFwCaq4DVtvYNZcDSTJCrxDjOS0bivQ5II7lzl3j4TUjOOo7/XcxDO0UhKh1fnmt0oscrfzFr9SSWKzGr14ma6v0nxpsf8Urr4ZUTpNQ1Kmkzn33EAeweUUG6qDBZWy4UxgvevRG50rsuOcyuMdHGkNqmsaqibxPNbNevC+FeLnaGABHo0Od6KW5ii/KZ1u0f2Gknh8Zm6PHQ6JrltQci3ENr8bTVLedcnbfy1BQxYsOMO2rRuDqz7Z+HUO7Sk2lLoSQcdjb2LORrw9O08MAsijb1yYYWcc1sDjr1xqozO47qRHCjkMydSfaVKiaVm+HCoRNbiby264Zh1a5Xc9eXPvXXZrWOgzaPcHlyfc+pl4Y/1TuojME1qfaxJahHpLrd+m9SQv3fvVyOZeek5PeNbPFKYXEjiK+LkQOoP87eG3rpHOx2bIeurCk47pplVjh5gv+tMp6vXM/BaH/CZTfPed3rSeNoD5BPru+3Hqw7k+3ffnbq3X7NkPezCyuW5dRyE9b18HVw5H2Vys1ObqW+vex2VjR9FgOJ0XvynjBfdadu1t6abNWT+7nSmBMwY2ULNFRkBdlq0+2lIPx7EB0fTqoZHDmT52h4iyPr9gT6UI5o8H2ZY2fY62Hxz4cLu4Gk/S7N+VvmI882sqx45ZsnfLkpcOaNV1vebvVjaqXo3YXf1gqxtYPHHTPV+mDA0of4sDSkk54fR9BEEl4OPFOh6aL0ofljDGAhL/4eBuPKMJBngaTeVE1jLj2Og+gwNa1pZeXtBpRt5Q8oeIO6vfJCjpJV3dJla1Mlpa1yWQf/DFDTXta6Wwv2dkmv6zabwLd5rISy3GA54X/U22vKcRPKrIRl7basv6YebDb+/JLpNf63BpMjo9P2/y3dvupyJrrSEHCXL24ZTL6zTpdu6lSqDpr42d6EUOnDBZId2FZy/xH6H3HhclC+PZqnvaV11+tS+gtoWIfqW61mU2eElSh1zlVqvrfMHuv1hCDF3Uh8sZSljzgEx4y3aimj6Ltj2iY266lcdyzrQPjEhD/S0UMj9zBhUvw2tKV9lNXJ25Qmu/USHyqHvaQ2ocPZCe1hHQot613jegeufoviMonTOuP2ih177u8vyLqd0j3Vz39ttw4vul5PM7A+n41qdfWop+ZI992TeNCt2fDU/x2YfjqFuefMvuIzsb+qUf+mZigYRe6W/XvXnWsYWpRUPuKUHqdVUd4SbYlwGzf8e+YcpqlOXjua8WsxT2M/vXu97DXfko1CJ26smxBZvU0jrs2Iytr3IgyfLe07/uv3SLWsbRWR7pnom7NrWLrbmXY20jmeyxto/q+sMdn7whyovdElLXq/KNMxU1Tg8mlxdKIurovZC1otw3xv4kwmWirBSsJAe+nXGI37C19nn/R1cMYmFZ9haaNnSfTe5/DG9/5j+32T2U1+kI/CetznH4lt9N6J7f3Y74eSvRcwu1lFOiZb194OiKaN/v8o1XYb2aDCeiWqpQXI/LJayZEXmRLjW8cHFigNo2dUgx19wIbaWAC4Ddmy5gT/W/1yN1vYGUptRuWANRZH/ZyQQC3fIwwTJu3a0n0Zgqb32RJKSAbjLU2dEqmloZtSFlZoHqEN2c8WLR/2sokAh/mAkkAkfnQDTuKvwYDPjemgElvSDgNfI4xTAJH4R/TAM2cYrVgIEl6SUQJ3FzGbgOq1l7Tl2e4WA3qjlYY8RQPW4JA1QM3tSyJAJkDBA1zy5hXJASU4DlAgSUkDS6dgIEkTA7D0O41lSwLED8ybgM0ChAysV0D/aDwPSs5AxQUwCrA77zE5PA1z27IggtwJCDzaYpzkDAAyIKUDogsIKusbmBIL0CVA0Uh2sFAhwOSCwuTEjSDxAjIJZYJ9YwNyC8xdz1cClA/exsCygsslJIfA6Mj8DYgrQLzEXA4KisDqg4KhiCynFzxSCjAyoNuUmgnoICDf1BoJ0CbAgwPsNsWYwLcDOgwv38CWgssi6cBgwoImDMg2/wGlSghYK8CUjAoOsCugyYIg40DaQNqCQAEQJWD9gsTkOCyeLYOaCdg9QKsDxgg4PWDPNSCFuDhgxYJABsg9oLcCngq4JeCpgv4McDegvIIsCfgpQL+DbAzzQqDgqYEICDfqMYP0CAQzsnsDYQsoKrIYQrmGCCJA2oMHI9gyEMODpraQK8DOSSm3wpcQv4MeCkQ2ALaCNA7YNc8ZhREKKCj3foLRD6Qq6zmF8Q6kKPdUQ57lmDKbeYLuDRGDNgaDOg0IPZCwuS4QaC3gtYJ19rmJaweEBQgQKcDJQ1kKxDfg7kI2DzgtkKFCOQ3kKpDmQuwJOCJQ0PWWDwQh+QkCPSV8haESnPxjvEjTGp05ZjFWPXPU6meW1CCGJFKTSl0WM0yyloVKr2F4PuY+UDC4xY+UilIIBHnZVifFsTj1emDCV4cq9N0MsCDrGUJNIkyKxjQ1z/aJihp03YAISZ+A2WSpIhFMnjXo4mIOSTD2gyv0sZ0wx0nlkswy5ksYkGKlSPtP6G71PNMuRM1zD3QnwTapOwmMIVC+LA10dk2BXMPLDombsMWp2qM/y7DQ9OLSCoCworjO51PVsODMy7bhh6tJPAPRY0f7Tr0fJ33I93q03hFFkUpEtVESHCtvA8L19EWa93/sw5O/RmsqXG/U9ZtXUPUrImAnrx18eqe0MPYSXOuxyYkWJp3ltd/CU0d9bpRxjj9JaHxhckq6LzhYAQxEskYNCtTGRiEadKM2r1Q9U71a5baRb3d0XmciW+Yj9HHUNNEJIs3lssZC9TCEFpF5k05+NeTyutvSAskXoW2fDS79ifAvzx8t+ZV18owhMvyWsSqfH2YCE1avykUPHYgI3J6IhUgcdK+DP3QCNxUeh95WXPsnkiU+RSJl8HqVkUUjmjFQWLcHfewyjkBDDVjCC6uKv1GYsKHq32k5udm1RFTwoHz+cwhcgM3YYJIfTnY5lWSPtonw6/nojMhR/kZ8whTskCkHfD5jCFlQlz2yswhTl2RJATCiMTJvg70lfIzI5KwsjxuKyI518OfSM/CLTAgM9dxGMLhSUi6Yc17NCHEslcicHCGWlomxE/iutKyfhga9ZI/yIelfqIKMTIQoxPQhtwogUNGY2omKMps4ovhm3IavRMmFUVrFKPSY0onDU/D+VLKLSc75KqIZZgZd5iGtpWfDmi9gGOzUbFbIq52M4HHWFSQirxS+gN8QGI31v9BwT4SfMubfW3oMifUURs82qZL2n9PKEKSMDO2WowtJP6ZaK4FLyLmyWjb/FaN7EmDCMXKl1om3Wmjq6KqN64PtbSzjZh5XCNNJ8dCnj1IMrY0xMdTJXaP0xPdTlmX5CIlFwvFEJTv37DOOCTjIjuRNZ2xoYlSLgRVmLS/i2t63EsgxlH1Ik2X8awo1RCUTOKN0wlkrXC0/DqJViMk52I26JGszKbmLIjaKEmLRjsDBFmXFGYuuTuUiuG8h18Toydg195YzKO25ybSynx9iWeej6BUzAYT9N//EGI9thtZP29t9yKoUfc5KfykxZt2d0gqixXY9mPNvDGmjRl9MJv2PZdHRMn0d+heWNoNloh83U0gvNKJ9ifo7/kOMBbKfwVkqxR0w3NXlZaPUcrovsSQoyfWMPsMqSA00itDSQy1bFFtb7jZ1TGQKR998dF/RFUKdbOOWiEKAGPTii47sSBVS44OLTjPrdwX9UYhbOOaMPRbSS1j2RDyOrjDjJOLHEhfXn3XkIeZgyvEdDR2iqjgOAOTctHYwmIrj5DY4x0ovNEDjW5NWbKNjsEbL2SnJ/XAxQEdbmGcw9i+KcChFobYhSKWsf1LSlQdN7KFz6BK5DpT1MHzILxNlGZKsT2FFBUKPpJcPHyi9kkjHeQ/Mj/VAOwp5be3WjihaVih2t2KYoPASZpZRg6ZEtXNkzYbfeuj29BKF2mHiFSVPUS0gmDhzy4J5DLlSCXmYbnHVVbP6W5iCYoeIBiTbGnSbF3GBBJni1RURme9CuKikDCmfX/hspiGNI0xdj1YtRUMUpW23RiVjDTnkoA5EHg/oCuEsR8YOJbdlA5u7BZWk4qo99T3sJSdLkAE6JMznQNSPNKyaF5hKMW6042MBIt9zTcq3GMUBePUC1WTdpnQ5pbfK3jjoyWlitJfNTo04klrSgyTpP6RO2JdVDQWwK0pnLfmcE5rF6yZ0YJcVxzcdyY+TY9IY27jO9CaAy2eNcae+K7pnrUy1O8drfgzO9GA9AL3UzDIiwF99yAGi01EqOJNuVzmRJK1Nkk5e1AkioxZhf4L3W7iutUSWnjf9YdNLmlsdrChVPioyexn3IMk9Rw0MaaY7xiM9+KJJuMy3CS3wFtjLpJaTl7IykxpN9OxIjs6kmhmKDDDVlzPid7eS2aTCyK7jC4kjWOyKNmBXzR+j1PFxM8pk6dxP/YieHmn4FlNSwToozuZELioAEowOHJpabXx2slPBU0pcauR5MEpnkxUnOMY6GAPdC1IhQwBSNguKQ2CrnTkkEUzuAWP3krwmrkVFSSeel+NGxfBLXsEU2/lyo3xTfQxT9EtzhMTQI+UMATfkruI8EpuAhPgcbqDBJtE+JA7QgSE9Eri2SYEtQ0wSRxHFNQDGUpQJZ0vk5xPRV41I6lejQ9ePQnk17WxQxTOaRkxb5SU7Xhb5mklANAibaSf3upMyegLqSXuPG3ZM5Q75PhMzFDnDwdMqXVLxJmBdZk3txo30VH5+tCn13dGUyxiGNCePS1MUxaQnlbcf3OAUJ5/+Q3QAFMybKKhcsTJHwiTkLOULWc1yVcwOl8ko4KuNQqQw1JJ3I48j6stWHtjTNvzGKnl8QY6u21MvZa5iPt0STyhosKIkkh1iwovSlTNb7UKkqEd9JazQBv/ItIpIkxQL2UEVlLKm+9a0gpi684qE+nDNQqMiimtHSUPk7S9KGr1pi2Y2QMWDcSSmzMtVAp4LV0h/biz0pA6fcm40mNeNIM5QaBOwjchfE4W0E/BWS0ZtnrSy3cDQqExLb8I7RrwM12bMdNbTHyTvgOTxg3eihFsKG9LPTQqV33oNgBY0gzpm0h9MrTB04SL6SVQuKhhCJ09YKnSFLfgTTtT2aW0cpPmURiPCdaSZLfED0gtNroTbQeiclkMnv1PZpGF0nQzhGGUk1ZV2XZWW9RaWCkxpx05OKOCK3aWySNdSSum+MSMtOmFDWea31O0oeN9ULjp2KESf9T076nAzymKjNPZ7eBsgYzzGaKk7SSSFaU7SsLO5UkyNyVxhkzy0vEOEzV2BiOEzQOF2hkyOHVO2EyCuSlw+D6XV3SsTJktvyeZcXbh14FnNYzMNcuM6jPfiwRVAQzQyreEQzRxxaWzrg9hEdMJCHKbozb9oyYTN0T2bH9TO5IM6jLpZVyDlyflO6ftI1FkM3UUw0tvFTPltos63zFIdDFv2OiOfE9Kiy7E6jK4E3xQgQvCwRLjKm8DNGrlBEfMkJmni9rfJnnlpeDxx5I0shVKdSKtfchq5kqfjJgyI+HJOvFv0rfl98Es0KijJApRLI/FsbfISSYVmKLMu48LM7lvFjRdZ0ht1OBQXs0aowBXWcZUhsL0p7BHQO3JNk5Xj9INssy2iZdsmAXHDQqP2SsEQ6GbKDIhM6jLJZ3BKLJbp2s4wRzJE7NACjIuMoEwEyrEpFmgzzGWin6DxM77PsCpM2imWCZSdCkk5fsoTLnFfsx7OQ1fsk9Pi5fslTNI5VmGUlIlEOWbJQDLMyG29t6MmzNgyamHUSdSLM1mlQF9sg6yczLs2xTczCcorPMZvqc9JpY1GGLM3EvTKiwE0e5bwQpYyjdSP1YR0zKgOTQI+mGnMro7xMwYdxAaRBsuHGLPLScDQS18jBKEXjwCkjPgVpo8kn2hjFJMwMnMpBNBjI099ckERWtRknnSe1JbaxkJVyon3kUZVyRVwep7mBTz0jGuWsnAZXhKEVoNDRf2NXcPM+5K8ygtHzL1i1DALJF4jLdDKJljzYuRX40ow8M9yrfHdLas906GUWYt01KzK8Ys8jT7pbYqQ2sYuXcXhrtMFPeQeFNtVXPqMgnN+lFIAGMvJ3DhvMym6Y0ojnym8nvRr1ZpRJMOPMM5QsrM+9anICkUMfM6rIApHvFEIRYnjRxMdI49Jrw2kb/QlLazG8uDLHNjDaKhq5wE2fN0lPE8OKFS2/D3NpoV8r3LZpHyDRw/tDPWwUsVehW50B8+fVJwR5EVWFMFcE9UVW4jehIxiICO8wnNuSFczvOTzy8gfIZ1gGerPDSlGBnS3k5QtrKLExBBfOU53chPRB5d877jdywRLfLU0sk/UiX40NG3IEF3qM1lHzNJPawqzoAsERMEas7sWHyrotKI8ttuH/wjSQCpCS4FwC3ZRsp9w1APATDRckLbzMqdrU3yoCvTWS1kCp8l7zWaLfPMkkC2AoBEOC3TRA8olBqOOizE9li0pmAiXLQEsNEYQLsjaJw0yCU6UUiwS3GBymgzrwkGPHp/jCyg8N9csSIRdkskMWltvrPRJ+jRc5czitVcnug09iNaxglZuFdVhUoybRxiVItrAO1ot10qKyL5tc/zgn4SDH6LLV3BaAoYyw6Zykq8TaeykCLyaOr1fprUiYxVSJVWQ32tVc3TKxFKvbOgDSs/aMkyLQ0gkj1zCi+ljIpP8qLjMFPyZDJNt8fRuIKLnC8CMYDkJbJSDI3RfXN0ztKAqlvISi5wrSYhdR0IipO+M3OkVKi31hqLPradiJFSi5UUX51TPxmgjawmorZyPSbphQpQaETSc0gxZpQNt4yTSmSyjjTiS+lvvPdW2L481G11MFgmMVs8h6ANP5FsrUzJ6LobWqxMtzi5D33S7C1+mjDlRSNyG0HLZos6ka5O0SxiXAxgNbjNc4pJetJIqRT4sqWX6nnZsorOmSoi6HNn1zQJLV1MCzvS/h0KP2db3GSdC+GhQo4S3CjxL6haDJrIoowhzQS0c632WTIi4HN7C6SpI3WFySh/lJLoOV+h4CMHBUnOCeSyksiLBnGRX1i4LeKlpL4I/jh5yrCwlLIMVxeuLhizGPIpAjLjGUtykU3BwywKVSisXKLFuTNylpUeDx0xVU/eMzNYVdQFNfotS5osP8DI5ciOT4zXsL+9hads0dLSNPBxxpqS/Dzm8qHGxJTEt9Ckm6Cviq8NGp8fDdySN7FehkvdPXSyUdLeuJGU1ywZDgUIt4GDCzJYqmZDJPELxX7zTKpta32Mo/6NMoOkwKZ4zUZTox3IoyCtL72mjTcNMu98tvIshq4e6IH0tzHStCl6TpbSEuqyvbWFNLYEWaDO3pIyrDK95GWOYSwLvk+3WCEKhI6MJSBpEJiujJM3jmUKiyhw0g9Ri+WwGTvii/KuTUA13xj4TmHGjCND7WFJQU2onGlbI6TQlMdMGMjl0lSaua8WvLjo/kwVNSyvY0vKJSSIUkFgAnlQxZFy/5lULhKZTRN9WTbZVS9ujUCPYZLfJ/PMNGy2RiDoi6VWQgC8dTDVbLIWFH08orpYALpdeApvTNdYk2cXPkF1PEtvIbKHiUvKz6CuIzztqB3KxL7ZH9mHJsKzozadrfAaQ4VGy3DJHpKKfMykts/BsTwLpSytMeih008kv8SraykCoh/P+mTS64DjlOZkMqUnXJIIANKXcuKg2jgLeK5ktNYBKmjSj8DrQKXzFDRGNMtL43SJx+4JK2n0jLqyDEucSjceckJVOWVsj6TyaNygao6kopw+CvAgZIFkD5dimOpyjFSL4YKNWdRBFgWCCCv1b4tSzc0oY2xJSKkqUNKroJeaKrKKHQy3PdIctG0X9CnivBX6LTJcypCY2qbMlcY1LJqgYzXOScJV1NWOI1e9MlaSQk56rGKlAkfS6KiK58g3wpFL/2B5LRybSaenarIfVQoecjGYIxOs/rSk2ust2ejXXJOzEatzSiq8tJcqfchqTk5qsjEhFoNhLqirEVyD7hBF1GCCH0LqhRit0YIIWNgzoVpb9geVoEgAJ05waNMnAokqnXN2oD4+ciPifKovi8r/K0uVOoZK/YQ6VTlVTUyrqnEly/KGndZjb92BHd0DzQPALgG8aKrcKH864f3P4Kn5O01xNt8wrK4lFskuiBqSqmCShF7iiCP85p4luV/TCc7GoNcgmWuSNxCzf2W5MK+MoIggwQh4TcDQTBkhBNKbe3SECeySm3hNKyjCR9z9yaDKjlEgzAPciGao7jRNHSgWgGZeymiKBpz06eilrHS4znFrzS1mrxJMAloKDM2TVWp7peayjmfKrikUu9IkSAcEqEQWV3XbTnhLCz0NPw6oIHATU0UV5qhU7TSW18RI1SB8VyI5gJYzajjOjJlo0E2tq5Qw1iyc2k8vTJ8GtW2vICqtKOPa5powqnoMlqLTU/DVC19XDq/anURuZA6o6gr0RrRG3FjPXbhwlC20hclD4Hja0s0lfw14TnLHks3WI41VINIYz0KQXyjp9WW8nJojc630hdF5G3IrdXS+Yu/cvEg9J2y4LOWPpCTSKulvSWyQExHqeotkpEY0cy+XHq75ZmpJLWBGeqiozKSCG2MzWajjMpy6AepwEc60ap15SzbWRzrTqnskbSglNjkFj1ClqkzJtfeQoQ8uiqOv9KUfLXwqMPKSayjqnrAJNCth6hw0FYzKeEQAKB6xCgAzJqr32pY0cnpxAbCqsBt280crpLaqPKeeJT457BUwCzvEsBt8Zeymq2WYIGxwx/rYGnQr6iX6Byp1NW6zotMCgoiCBwFe6vnyIDCSJTyEYOqt7L7IWGNj24UsyIShAAGuEO2spGGNEm4VKAvziB0MQzqxiFHOH9XbJIXWlkUlXOOUmr8hab9S5IZaNqjvEKSc8mpJEvbUiUVkA6llLVzA/ugVJOyGBVBpq/QSkvkxdFEjpUA2Dcjo05+YUnNo5hbkm0pSxLmBSUmSRVhAp+ePsh5JhtTphAAjpBuAMcVaISkJIWKQchRoqSWyiO4kASsm+Y6SDsg9J/WKzzLIlqfji4bnrRMnVM6NfxsONkSQcjrgbjHEnqCrKCNNhFbyJySHM2yLL0ggEIm5lhddaN0s5I5SKFMlJLFUGO5JC5RKmuY5SROi1hX1VJv2l1SLeysZQY6uWCp1TJuQ5phag0jIpK+ZnyQVmjH6iSpJ6RULx8900hQxFRsikmwpCAG2mNQMRFsglLBKIow3FJPAtI5s0SSgwbglJZWrmF2TCmmrIxOTkh5I5hPzlh1tOUI3LjpOG909JaSGI3ZNsSWrkJ0Q+FIy24JEpDjezbpP0mFIcSKilG4TlZEnUdLmtsgopvBCt1ulzDdz39YGSBfzFJoyGUl2b0WEXNESjpEPksovfc2RO9hKCCCckYmjnD3UZSc+g5dRKXqSZF6klEJtpQW4SLmEeuLkjeyOaDsj6btCEBKEodGlKhuMU7M4JYp47K2kTIYWc+gec64JkRbJYGlEkGicSbWNNwjyG0ic45hFcVtkh6OAQgg+mgLwzkIZfjjIpz3H6ispZKS7yzI82UShhY2yfjh65SaCmjjFRuSim4VbSPc2xaTbVON64uG20mrosm2mgrZ2yDEIJaPSL/QHFzA3bwHE66cM27pcuNsgBahWmZys9+nZfnlbeZDshmpTGgkhYpjUAkjxJ/WdhnZNr45En0x/WbQklIOXQA3vd/GrkhTs0SP0gbIx3VxqspnuLEmRJ2TQclmbKKNAHt0xOKMltIvgv5pxbEGPpsKoa2sl2MYOXMzOXJT1NuwEaFApanEZUSGFkioXmyBXMDr4sTg1ltOOlWbjq6OCKs8yXdkzIpN27QhiaKSFJX44UqXZo7IBG8wMvbwmiCDgiX6VznZMrRa2IbgpRUMPPb5SMCPlIq0wgHbJrGI7m1JNUyFplJdvFNrRJnuf5gFbwUzEgTA4AxBgATgqNj3jopWSKn7osxGkhh0zLSmQpKgqAKTgCYjH9Xha2qOpvPdLnCt38i2yN7OHapRXEjGoNRH9UVY0LTEnvaSxGPCckR2YAVcbsTQV28EnJaWRRU8Om4yEoa2uUmpJHdf1irTlqWHRFzEBLkixJNuWBDXJOSJkj1ZlSckhSapSDcVgb5SISmKqoqSTxMpySf5kFdv1Dmis9EqGxTU6TGgcSzEbjKUQ5wkyBCITBYEA5lRM5hDsheb7GcDqEpkSW2TYVUSFskZJjUJzrHLJPbhSs9zZBn1vIK3EJuro5SFI2HJhKZfnUFwcok25I72zsxSMSpH6lpJgJUSj6AEwUsRSNzZHGhZqiKQcD7Jv1PSgpdwOjmhmdi2pkQhkMSL4Mc4MQlVrjoYmh4USoK2u+kuduSaslpJL6P/SI6zLPdXRIZnN2niaTOtEneyUSXZuRIh2jPmxJt2zzk0k2FcwLYU+modqgsgBPdvhbSqbXUTJG2z5iU63s3htLYq0vzkxIK3SkiHaJtKUgxF5u/aSHNlBXhnVI+mNsqe6SOBrhAonujEQip8KQnTrsJtJkW9FG21IMHIHhW2RtpC/MxlNwhSZ7gz4tnD+jq6ayehlIY5G6umQDnu79XUohzffKe6bSSimbKGuaWUdZgK8wO/VBwBrhaVKneOkhd2yepNfVbZQiqAFNObFs2yqu89oMU+mDcWQCBYGJrY9Hmv8Ce7QaJ7WYoGLJKk5DQjZ1pjZ0STkiHasQxvWrbbZDnDIoyXLlVBpbSEcTlIuSGBRiFfjHXuFJzKJ7vEz6SXZpHaf27kjfVhtDxkFcGuMlx+pzA6kkJ1cugqgTAq0oih/V+ea5nRJ2TcXi47Y2r4IFhis/pQTAYjVwRmFzAq4PDzQjSTzxJRuGJuroH28DsNrsKHVpibS2ajp+oIqKVg9JXSEXMscGOthTaoxFD+lvJ3s0tly4pRKkgvbYEFOx5a+yMxpRIuO/xo3EUqW8m6syXXXqclquzqQHAZnE0k/bKSdk2uYTWykiFpzA51v2EYWNtUfsnSIdqlZQ+yPESbJmxVlGsNRcwNvJYddEjRpBKZAMQYkunRsW6JtFgzOCXXPziq7dvVRPt0qSd5vW7v1S5ysp1TTf2FINRbsuFJ7dOYQYtVabn0vlcjOUiopKOZQVtIquk8l28zLbwSbJYKVphsp3JCFTHc64XZutqpRZLp37H26tt/7sSN9SO4P6ZAMFpZG9k12aNRKLR8YYhCbQONBlEqVqbB+wShFIWKHXvwpNeqCzLJy+xVjfUm5Fro3Izgr3lBp1QhIRhZK2xqAoGhzZEjo1AaHXjl6OXVeh36Ydav1iaLI/xr5kTnAVp0bnuR0m4VSxCFSv7IyYKm/1IqDEWe5nmxmuud+6fxqg5e07TtIZFUmDiFIfqKMnhFKyW0lCMUqCknlbrmHZqzIyyAxlLZVRDEOuY8SOCNN0UBeNr3kK+r4Il0juWBCf9SB341xJhyH5pBbSKH9TgCpWXZqVY3Um0ixC6SRzlNxxWqpsb1eGn9Ss9Eva+PeVDBV0ixCr2lEnc7Evarm16GhpuWrpuOo7hFy79I6XA6T+qzrbJpW+9y1IOyIQZmckFYbVRIIONlrb6f1PskpJGxbkkQZqyKUU+ZdmvEjsoyXRaqlZ1Qtsgm0hzaUn2pVHZ6xQpfTY/u1iQBkOyRIJmlJVOHbooNuz6SqVxokSBxFIyHaotYot66YTKUSKoMRJqk01/G+JovazLIcwpEG5Rmr6aIOd5soCsyWF3fj2go4KpIbaakhWG6NZ2wkp1TSKmRJzAk71wGpWEjjIpBwdUxZqUxP/Kyb7+o9oYtq/T9ryYIVCUk5IpGm4whUoyL332kbSGIQaUiOzHq2dre/aTSc2PDESlFx6EcRYaYhmIz1S91LgfuEK2rANfV/WSKlvIJtNpwFhqWTbs7NtyPdTgisSSPAg4hzQnWSzBBpvXK6fGdU30oOetEhdpf+MHr6brfQAai7G9Tlp5INnfunVMzg2kj0YOcCFRmdlyJkicklqY1HaoaSHRvsbkAs+PoYs+kXMoZu6dDxSNalCCjo0K3JAagskSRKgyb0PMy3Q8ZaPzm/UKSKHu5zmSYUi99tOe3TOlEyTsgVi8Rs+MJIe+uEbL7fjbwUHIVhtHqHNAm5AP9YvfNTru6J06+PCdq6VgxSM/9QMfl5jUeoS8KjU4epYpvBAIbbIzWG2nF4haCki96MReswxGyXW6Ssom5YqNtJtOcJqdIm5e92m46NLMgg43TUthuZJ+expQiq0vMjvHXOBrtEoGLVzmUKwB65jfDwOp1oDJCWvsgYsfGBi1tkkFIwfH71SA1r36NKI6Xm76zM4KSKe6XsfPo3smsbLHbyIWnQ9WyLPtfU26mWmlau2kofW5KyFphbaoLe3TBHCGMkaV9IWvTxI4EwV8cGa/ZPpok6nGrkiJoJdJhirSlclFhcM2PM5jmFsKallPbtYm4zE5ovJ0kpIcO1EiUahOLEKs99ay52illSF420Nbyedz1IVRvkIhU8mo3FxJ8KZ0Iz5qOthXObrmGZxg46g1GkrJHRoIa6bhyOgYRdIA0Uay6oyOKPaYaSesxO8NxnlrzbaY6dpYpEvQSmesOqhcVP6yKe9zrhv1Ml3HZuybsgOMnJN7PQ9USRyduivRqijUbxeQJog8lWyF0nE3xOyh5JkSJySxCHhFOyzSqKcTrQtkAv0er8Cp1zmX4z4l1s1aRco6UVZTcTknPHN6JPrW1ONDLoqbbSKVlWpanE8alJuyodqsGs0sdzbIex0wXPI9Um5kJHDxySMn5f2DWR1GiqKrzMs3HRVhJ6mREEWPN4W22TnJrmZ7m5IQ2q2iJluGIqZl6ZRxViQUoyPEa+HlyHxmMaV+20kMZX1dDw8aOm0OP8aIu0sQ06nSfof9ZzZIWjE6PujBXJC7qTSTNHEvUdpE4nJMMlCM6NdpghUsSKrwm1PmMjhiacqfyj+bzZIdvhM1RHCaNwc7ZDvlIOhujQAmpWVtvxdcOkboa5xeJUj0pFWOAKNUUqFO2zaCp37oXbeegtNNwBaavxJHT+xOsUkFOuiTmFzAmsi/IsAkPmfkUKJBRmcUQy6oPGSSbTm2MQ6EGdcbX+pEwxJehj0iPbtYwbKNwUlYxmYpKBrhvw6bjbh1GbhSWbkc5AZy2n5Uj6CGUENczOdPjpl+TbnKEcJ62p370yFMilZOBDmiJl4J2kiop1M1nUjIcJyE3laU7bW3mEoyJGZvJNJcwILHnrdSh8YROURPt0Uqc5pbI26/xum5HSCt3fI8Qg9ttIjcYlyIoSOW0msomFBcj9o6Wykm9G06SuadIFGpakxUT5A5nzo2h66xUp+h1piZFfzWeurb9pcxqpI7qFRvHm2PaSjHL+OMUbRa/+/PJXEBxMI05IjpLzur6GuCsjapQjQdtdn6hJZoJ6ayNtvUpLhCL0pIGuDPjSamSBpWgEUaKOS5IcmxL1h0fqHxgqaNnRJKlE45pCn5nDaImRibXjcroFh7xbhT/cCSXI1lnVtawzljve+kiO4apibSNwxVbXgrzIlXxgYtUaJSIZIt2srqSa8SYQaibQmFUacksBuyne6KSbdpF6Q+XCT1IffBmaJllerzp9EjGtekipKyFElhZaWE4ZL5E6H9XZMbxi2e0JiJph2HHXGjPipsGOx9RiMq0yFwip6GyCVCYvfauXJ60Lfew1lNW7Um1jX+KqcFctGpyXF5+hG4z+aIZ9+YzmFewVxAo5hvDrYVIXIilLZHOivvGScSbwW2mRSYbpiNoei5puMATDDshcG4Lykc42+m43jtBwLMSDIiSIxZWGHmgnsq4kOVxvclX1FNsHB+ODkgYtGxA1vRIPJKhe0IvglDUHBt2jheuZz6IUkMY16zVPRyRc+9298h2xJg2UxOpagg4lGx1tsozLFMcbEMm/d34W35m92+NbDbslcb2TLZ2rpV5N9QASHhTMxI4daTRrnJEGa+LfnOSASfZGU7R5pSNLnK7ltnbKURPeEo5ERYLYyXJKilIKaFIx3mNlgcUrJ2bDcnjohSbwWYmiZakgn0oloCeja4IzpuDb46Q7q98/KUoXKZQac1sP7kqIrinojpCCAwKJ+FEixDdmo6WxJfGtZTJdZuQhgGkkSVznhHcSKFMRamlpEk1b92A0n+nRuBi1cbOzOUgxFhtdDuG0TZLEOAHi5hdIg5c57zVukhKQZXCkiwj5zqnlybttCao5fgbfUt+4JnJIuvTYe7o/wBrhuMv2pfz84KRsim2bYyW0mh6VO29udrXxjYY+o4xc+lhYdaFNqEoFWmFlSDFWZzJDIIZW7jJr8mwZdjJGxQLurJ/WVETliROQcFvIn/dSktaMSOFuW4c7Ikp60/wVnrSUMSIjxybLnGBTkoyV3OdoZK+o6X3YjuC6csVIe/yihSYe5UmpJq6aLJRCWe1Hk5C6SXbwxJv9LnrfnvfQEeFJZyFUcLb3Xa1lBobmeOkJ16GaqJyoSp85twXKyB4WrJC2m93ZGXqFOzM6L2+gRfVNufIY1FC5FpkNnq5xshtIYjRkSHNnrPltBae2yBZOU3pxKis9a0hpi4anyROeM7dvF1sDt3Rq0bbIDSN0jjmOkhBQfSdWkqSQUVxjmniUHhE5fanfVPskEmC1qXs+oiTBcXQ87qFhrAo0lbqwXFT26ul8WNxHajbas107mD7+SPHv571KYieoZ92Hopxp+6Gu1o7qjfaSd0quyReCq/wDlyO5RKUcemTex2ykVYiqEAcJ15qTEiIlHObTkS6NySskJGG1nUlOKq2xSTjm8ex3RLkWGGsgeEBYYPpXZlqarsp6UaUtavZ6p+xh/bPR5NjbIkugRtyttxxSbraHhTkYGlOaDj38H8KJRR77I7QcCl6PnTIcMG2PUGOyXvuOYUtoJGit0XMm2jbtvayKBpWPGbjMsis9vBWHis8R2VLyZEIrKtKq7G2v9dRJnrLLzLIhlwVxOmM+cPIy7bKKCJmFaWCmlpbIqb4cJoMh6CipJfjJGklI6lhtZw19GvsgTAh8wGfZU2PQubjlv+r9YW7lamPCUbBwW2Rl6oIqVk8o8yXjmeV9GvpoZIIuvsgW7v1L31pYA1qtV2bsmqVi5JhtfCXvcxdYEdc48lSp0ubhKbSis95+vUdimwerEhYpPNikltlGqTse6ao5xmrA7oe5pur9XOHTppXfjeE38ab3DZboGUmnak46XZsVXK2a7LKm4VzZUlp64FzU7cmH/aOAO22cO5fm0IMqSjITBtOGWjMtKx2PpsU+et5hY66a/pT5ITyBToXEDWkrfeFcNwVwrcbmZfgbG4A1yQz452qKjUmTFose1jeyWLf9XS2NTqOpvaPdV299G1kcMZbl2TqMCyhuDoQoqKO5v025yBCL0oBYOyS76bN+KkkWQ5vITYmrZ/jieFFQxbty4SOB7tBptCAcGSaSyCFW06X7Mskq2zGENrj7sWfpxx7dm6fuk5OeujVtlQ+x0knmd1mKMgXJd8XglKvF663gGB/NdtJdSSYYb84Wx8xhV29+0bg5c9mqyclmIOJoaenv+8gas2ENBCKC3yBqkklXzZeE0r7BoucZWXE+sl3F4vt1Yad6UjCXSOZ1QiXer9IXXXeFdGSYg1soYVt2jqke29PghVXZFKih6pTMIxk7C2yecGWCRCKU7JC/UWcOnIXSPDRt9duAIz52GbWO3GfGeTpYYTSQvvclLaNSc7N1KFsltlP2oUxHYYtjWjY9juhpRhYF+8/3qDQaKrs7MyXGkgYt/GgKmZGmBjUVB6JtNyx1bsWX1Ve6/aTGet9OSav3vdYdGShYYOaX4zbHA1nsYE6iyXkmlbbZL7rSnV0iAZkaOB+uEbF5+jEMv7PlUJjB41q2kjxDBZqUjMt8994RPIS568QpIQ2lMTh6gFuYQeEWOglbAabjOAO+7DZjUUQ6X1mGuz67O5uLua2e2BGm6pRcmfA64AyVsinDSGsgwttjNzpSkG4Ncm1jNBgcWpJ6J7h3nXw+rpromEwTEk5l4WxFmcZjyXMyxCkx6ug9Gou0FcnmIqMHkgh5xyilR71TGFkMKpWDPjoWm9y0SzG/ZarlNwiZUSgeEsm0OcgnIqcZZuY2xhBXRaKRKUh98m92pQYtlx5fadJCN23vg4uWRFoiDsSFJVSHdxolnVJ8KVxrApOF22Xn2YmwSazFEvBQJa21uhsm1XKyV9UPHB+jDexSM+hNpsUFZ3LqlYjN0AzU75hphVumGLJuVfmBYHkeKnJSQfqoosGNsZP8TKH7ZFzouX4x8Yf531W/05SAWELJjJ+xebnEGSjoWWh6E1sucRc/YRCbMSYEcTIzLRLzObPeqVimNVip0kb1e5qro/oJ1tEifbzKaowHF64ELUEbf63ZsEpBBy7UUVoyaQYZJcSFIyEoIOEDlb2KXbTcyC6NZtqFaNRLMUp7FWN7OYm9KPkMza2e/luSWM+k0gkPIMtpdZULpj/uRI8djPnVJYEQbmvj3O1Rrezwl7TjgCpxME8NYIZMfMhbBRWclqKPZpldXGBwIUm1iIObUPWduyNfp+oUBIrldHiSVEj6aXWtEmBno22nSZkYhzeZ+HUu6jYPI+2lno/6PO2GfhMY8Xaw5H+OCbUc5T54E8hdwO4Vsc5FJuAI3EJNHrh+orFhrjY6rKbEYPbbe+cYxCmFStuLs9RsgdnnmlsyZgrOF19W7JcSLdsdIwRz5gn38qFJWsHi+yVeo7zjx0nxmkSLhq4aIOc+hx3nuO6nUc2FczQnosQpXMjr0WjwfNkUlMMihGG4BuHz5YdbwRdaB9syZgmQ+F427I+yfqaYHBwY6eM5tYjZxtJBVTHut9J6CXoWONuyM5Ro2pofY9i6+58dHGbSWtJ060t7ulca6NNEgg63ewSiboSOeYuLboycdtLaVKe3XVJYRSshiNrmY1E33IXDGaJOdU3b3DPDskagEWMFKrurI9RiPtPWmFKyktoxyc+jMmiZZQbgVwsn6glJDjEBjvIGRqUn8bN9rAZrJwO/MSxI9hlBXhMbyb9VibCR+SdNNlA7Ptg0jFyT3onLzhtfu6/wDMx2b2Zy60oOSyFg206YjfeXbJTujWVXpE58If/aZnVTro0aponptptYyPDQt9pBY+bnnrJ/2yW0SHmh+oz4kkf/b0PSo4nWTbOqfvdq1jCYDZyjgxWm3bxRMZebzA4raRIrW0GP4dRuDsihOYhP8DCnCWytY+GGRnsjMssQugcIrMNzkmJb4jm5kAndvPdWn2RO7khl3oejTqSlvGq1f8a4IhuBR2nJcRQzYQ1yFd07fpyPAFY27QjY+HEqNxbpIPTk6YoZIaQWdJGnVtjLbbhWjnGM4Y0kbqqmjuUHuybPGfaXhPGT33ohkDeo6XZMayS6zvX0PPyadI2qEqeamqmzEkhocSSFzbto2z5d9XLaUqZTsqvS+QJIeqMlxbIjceqJibIhiFUb1VxnRvEqCSZiZI5Aem09CFARytY7IEwW9TpIAJtskx7tdV9V72SV0Tds2Ke14+NRqh0HtG4hhoGfnWJBujWl6sm6DilYTlrWg6vmGvMmUPXRg8dJp+6fVYASw53ca9pyRgxU4444wnT2OmLRgfNo/ZAlpD482lnqLGi2xPslJehhHeBc31LjgjOndZtvnXBVTcUlJ9GlHv8oftwnQz5Ljkvr4yui6KW4ukO8Lts2OruifvdDd6+NTGpWSFvUp/dw3oghvaYccFJJPAMb5lwho8eqmjpBrm2Hl+TrY9I2FPvsVYQtFdgtJbyQ3eHH4p0ya4aMO34yIka5g8em2RpqshQWUqC1o/b+mXxiVaZt2pyyGt182gxJ/WXVeObDWBuFEphaded7bQhJ5vwlDarMlS7FWZfjTOO7CMP3n1KB9oX7bpKtNCNxXQ6UguwKCpohOUzrke1ILIiRMw231eo8Pok+6ulgQkSbhQfH3LLxe3ORSVHqYUDSOm4YHLR05a736j+1eVJQJ5AN0nPpyNtj6YatGeroUSa5kZmkFVg3MYBTpDj080SPO5A5cSJ7dzvkSYOeUmm5Gps+UYWhqVc4cpzGLCaMu8XjM60lIBhTvvaSKlh1lV1ejVaq9+Srcyr97cmZjqjQG8kZkrrMXwknJTElrnWm5nwyXos1U7tGntjZba52AhU9E3CtpBTSn/B9pjXrtdrauqMHZ4JY6q5SMchbalWy2kxJprq4OTmhSehlKHcaUgexp9yO+cudFq+k9wWqyUSbICbyDNnrMRWIWgbpdmuUaFI2FT0lNweNomVvZIXezf8oeW8A40769q2jE5ZYykg5IcSUPka6Tu1NvrJAbwFsGWg9jOeKvmr/S7E4ShkPhNkX6aCzHurt6ZoyosLQhYKnMackfuaid13sfvIFu+UnE6SVYe69ChhkjnamOx0maNn5sMk7NIXR0Yt7+6Gle4UZt/1hJGTFw43tW1W/vcc6rKki4JOg6Dlt7lC7kadOWKe3OdjuMSPIdlXCKtEigimGYU73mmm7SnVISk6vxpIlJQJrcsXznxkZmPuumpybDC4d12bMO0bngHV5J/NiudutUbHaoOPEgJJKlgWilFdguYRWNY2Wg52p8XPNmxID2kjiKnQ2Xbw5dBxnVsc5Cu4uSyOryq0bEb6jK0m98PnabmqFmumsgi62z8RtKHtp9SmhWM10dr3VUSOyVpI0Mgw8TGY8FWkEm9l7klCNPmG5jx2sziLb6bbxHuii7hVtqkGeK7i44I3nuIWnh2PGSTywMx2T7VG4HF5Y8+3uSAnpRIx0jPiaOJD/1lrFee8+7xHq5U58yXlW/jk7GrKTEi998vLhaiXRuKva98BxFJWi5r6DEhSGeuPfvgmIWeNvJ6GlJGdxW2PLkhvJjx6oy0PIqXhqJEr20tib6dnwV2NXX1UdbMZiDG8lTsEX6S7tIs1jZcBuYBr4NhFL5S+WCoBWDGY5pCdRzpR6yyRvSJeTvY5TLYEXpBUgCg9mW7Hc4oxldvJal5zmYoOGpW8EMqus5pYo/aZfhJWqnjPYhlguiTUdLPaa1jXZkE8JrQ6KaCOx8YYJzDq73lSHrhCamN2hfZUq04JhR6omt1bd73uNqg6lBs7ss56q0xq9Mp3syxgJEIISsjm2AWlHu4VXulOxwvVz7RpqaY8DZ8Jb5X1EwdHIIS5kmWjnKC0Boph1IOa2xyROvfGNlnVg6r12sc4E0BOiKUyb3su07rbz6I6S4a+m1UXNlS2FrywvGxdfrKGhKOHs0HR2yxQCm65nKiLDUm4bXKb+STISPb/Wb2TsUYxwZiYWmFHWghV8ro5lSbFzUbna491IWi5XbxVVqSv5lgWncbYRQpqAoTZgcUBbYxl0hIu+mz7dEoD2wLu2Z2loYqzETZJSKwWGy4ObezNRqTadJ9mjnAm92Rvsl96ZttmmWp+Tmgaj7g9lWn+vsr4gTem4pcBXddK54Yftd/J/YX+XbpQch1apaEXOtr46WZv2lNuczYrHYKcwx8ZDZuUdulBJ/auI3aWb5ocyayFAcS9UVuvaIWXNrMmwpV5MV7kWnprpxGo16S2ig6Yn+XTLbd1PKmvirZ2BEunWrVGeNRx1pt/85UZ+2dDY7Fz7SVJCllJubn0W1lQY+Vxk7oppk57wUiM4A6+Im9GrqATMpovPVhmd+mYZMa8RSVJR23eyKrp8ZoVqyc4X2GBseBcQtmUlJ7J7tcgz7gBFJWE/IIXOn8Pkuk1r/dy+0Kl83URMJqqu3Sthv6GmF5zgDXrkTJI8XVB+zoWuBnlg2MF7epGmen+OKc8k95KkPjXIz2jESv351ukiKabSlNP6bfja+Jg36p7WLYUtz6Lh9Hyim5bOCqumJqAXtmoOhcNSKEdd29rd6Fc7NdveY+0p5RWRejuBpUt1bIhSR3vz7LPju9NMV9tMhEDtVy+QbhBXGXom8BBBxfAGWnpY+k6Uh4MeBksxKrsMeQ7UcQbhO+2ymNQ2FMy1kruuqzub31TCMNspybpUl8bOzW5dJ7lGdm4hlLmGgb3H4BpkgbW91SSe91mO2HTj78j2HU76U7W/N+M8N9pkfsmV0odxJYG9Uh2aS55KnUc0e2JqvFcSR8ft6hSFO0a687xvX7e0M61piNIlRViFpIj+EzzZKO9g73lgLiXWj6IMzM152sSMd4rcT+tJc2bYPpuW9E8d0tgxFqqBMGbbMX5nrEmYriGSHM2FTn7gjhKNxdw60f+vphZurCWcOWyHpmUQZEmx3Yz5VbLd4tJBXJc5bJKj+aS56FyGtrap5QiHOCq4zk4cKGP6Dm1BpK53zvyXRuDkXJniDGFkQYd+8rlJ6b7j+g529thNrVaV+7wXWGDrPITVeFOJBRxo2POMj3VwltluR4+JibWP6UBvJW0IGRwlcpaNRZQ87MmFYU+/VSBi6cTHLmYKndazgjYuLnVj7nwxi1N8Dv8ObaafsrJpureVLFCf3dWqHQ+0t4MUQ6RsQJJGr6U8Ca4pb0l2b6xmShNoOXGskla4p0McNooqb5/VNXzkVtgowR6mY3E3m1xcdWEnhtZqusfw6c9uxOBbv93MaCfYR4nj5ArIWxe00h/X3n5rc3DEySr+uWOudv7ikSJGx4ipXe+I6ptv1SmYNaoI6shSMxJpRRvlsRtGMMfKZsD6RTd7JvOaK7f9Cmg33dpiOsaugofb/rQAqyhCDIczm0LI401fJaXvG0pY/YH5wCIZqGPTGZLdN0hjHDYqdmY1BC7SnYokClpd6FfYzOGJrMTHEgzDJBSLdQ/ijHLoJH3Qx67nfILIBBHYh9G0gx4OCKx9KUwAJccZjlJlqdmfN6CPU3TIBMTjodCkhnvcOYpKYuZckbsYDifrqCTLELutbkhHSL/RtnOjS6iYS7z/LO4n9aqLPuP9b2sdH4eDbn7PHPbZcqRWbi8DUiT3Tx4fdE470Caxg68bXRIdYy7ErMDqUbW0YxPXZogtP2iFbVailsEo76fdkQS7dkwCbb0B7gFYBAAA=";'.replace(/[-]/g,function(m){return t[m.charCodeAt(0)&15]})}("var function ().length++return ));break;case ;else{".split("")))();"""
-
- -
- -
-
-
#   - - - def - get_tt_params_script(): -
- -
- View Source -
def get_tt_params_script():
-    return """var CryptoJS=CryptoJS||function(e,t){var r={},n=r.lib={},i=n.Base=function(){function e(){}return{extend:function(t){e.prototype=this;var r=new e;return t&&r.mixIn(t),r.hasOwnProperty("init")&&this.init!==r.init||(r.init=function(){r.$super.init.apply(this,arguments)}),r.init.prototype=r,r.$super=this,r},create:function(){var e=this.extend();return e.init.apply(e,arguments),e},init:function(){},mixIn:function(e){for(var t in e)e.hasOwnProperty(t)&&(this[t]=e[t]);e.hasOwnProperty("toString")&&(this.toString=e.toString)},clone:function(){return this.init.prototype.extend(this)}}}(),c=n.WordArray=i.extend({init:function(e,t){e=this.words=e||[],this.sigBytes=null!=t?t:4*e.length},toString:function(e){return(e||f).stringify(this)},concat:function(e){var t=this.words,r=e.words,n=this.sigBytes,i=e.sigBytes;if(this.clamp(),n%4)for(var c=0;c<i;c++){var o=r[c>>>2]>>>24-c%4*8&255;t[n+c>>>2]|=o<<24-(n+c)%4*8}else if(r.length>65535)for(c=0;c<i;c+=4)t[n+c>>>2]=r[c>>>2];else t.push.apply(t,r);return this.sigBytes+=i,this},clamp:function(){var t=this.words,r=this.sigBytes;t[r>>>2]&=4294967295<<32-r%4*8,t.length=e.ceil(r/4)},clone:function(){var e=i.clone.call(this);return e.words=this.words.slice(0),e},random:function(t){for(var r,n=[],i=function(t){t=t;var r=987654321,n=4294967295;return function(){var i=((r=36969*(65535&r)+(r>>16)&n)<<16)+(t=18e3*(65535&t)+(t>>16)&n)&n;return i/=4294967296,(i+=.5)*(e.random()>.5?1:-1)}},o=0;o<t;o+=4){var f=i(4294967296*(r||e.random()));r=987654071*f(),n.push(4294967296*f()|0)}return new c.init(n,t)}}),o=r.enc={},f=o.Hex={stringify:function(e){for(var t=e.words,r=e.sigBytes,n=[],i=0;i<r;i++){var c=t[i>>>2]>>>24-i%4*8&255;n.push((c>>>4).toString(16)),n.push((15&c).toString(16))}return n.join("")},parse:function(e){for(var t=e.length,r=[],n=0;n<t;n+=2)r[n>>>3]|=parseInt(e.substr(n,2),16)<<24-n%8*4;return new c.init(r,t/2)}},a=o.Latin1={stringify:function(e){for(var t=e.words,r=e.sigBytes,n=[],i=0;i<r;i++){var c=t[i>>>2]>>>24-i%4*8&255;n.push(String.fromCharCode(c))}return n.join("")},parse:function(e){for(var t=e.length,r=[],n=0;n<t;n++)r[n>>>2]|=(255&e.charCodeAt(n))<<24-n%4*8;return new c.init(r,t)}},s=o.Utf8={stringify:function(e){try{return decodeURIComponent(escape(a.stringify(e)))}catch(e){throw new Error("Malformed UTF-8 data")}},parse:function(e){return a.parse(unescape(encodeURIComponent(e)))}},u=n.BufferedBlockAlgorithm=i.extend({reset:function(){this._data=new c.init,this._nDataBytes=0},_append:function(e){"string"==typeof e&&(e=s.parse(e)),this._data.concat(e),this._nDataBytes+=e.sigBytes},_process:function(t){var r=this._data,n=r.words,i=r.sigBytes,o=this.blockSize,f=i/(4*o),a=(f=t?e.ceil(f):e.max((0|f)-this._minBufferSize,0))*o,s=e.min(4*a,i);if(a){for(var u=0;u<a;u+=o)this._doProcessBlock(n,u);var d=n.splice(0,a);r.sigBytes-=s}return new c.init(d,s)},clone:function(){var e=i.clone.call(this);return e._data=this._data.clone(),e},_minBufferSize:0}),d=(n.Hasher=u.extend({cfg:i.extend(),init:function(e){this.cfg=this.cfg.extend(e),this.reset()},reset:function(){u.reset.call(this),this._doReset()},update:function(e){return this._append(e),this._process(),this},finalize:function(e){return e&&this._append(e),this._doFinalize()},blockSize:16,_createHelper:function(e){return function(t,r){return new e.init(r).finalize(t)}},_createHmacHelper:function(e){return function(t,r){return new d.HMAC.init(e,r).finalize(t)}}}),r.algo={});return r}(Math);!function(){var e=CryptoJS,t=e.lib.WordArray;e.enc.Base64={stringify:function(e){var t=e.words,r=e.sigBytes,n=this._map;e.clamp();for(var i=[],c=0;c<r;c+=3)for(var o=(t[c>>>2]>>>24-c%4*8&255)<<16|(t[c+1>>>2]>>>24-(c+1)%4*8&255)<<8|t[c+2>>>2]>>>24-(c+2)%4*8&255,f=0;f<4&&c+.75*f<r;f++)i.push(n.charAt(o>>>6*(3-f)&63));var a=n.charAt(64);if(a)for(;i.length%4;)i.push(a);return i.join("")},parse:function(e){var r=e.length,n=this._map,i=this._reverseMap;if(!i){i=this._reverseMap=[];for(var c=0;c<n.length;c++)i[n.charCodeAt(c)]=c}var o=n.charAt(64);if(o){var f=e.indexOf(o);-1!==f&&(r=f)}return function(e,r,n){for(var i=[],c=0,o=0;o<r;o++)if(o%4){var f=n[e.charCodeAt(o-1)]<<o%4*2,a=n[e.charCodeAt(o)]>>>6-o%4*2;i[c>>>2]|=(f|a)<<24-c%4*8,c++}return t.create(i,c)}(e,r,i)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}(),CryptoJS.lib.Cipher||function(e){var t=CryptoJS,r=t.lib,n=r.Base,i=r.WordArray,c=r.BufferedBlockAlgorithm,o=t.enc,f=(o.Utf8,o.Base64),a=t.algo.EvpKDF,s=r.Cipher=c.extend({cfg:n.extend(),createEncryptor:function(e,t){return this.create(this._ENC_XFORM_MODE,e,t)},createDecryptor:function(e,t){return this.create(this._DEC_XFORM_MODE,e,t)},init:function(e,t,r){this.cfg=this.cfg.extend(r),this._xformMode=e,this._key=t,this.reset()},reset:function(){c.reset.call(this),this._doReset()},process:function(e){return this._append(e),this._process()},finalize:function(e){return e&&this._append(e),this._doFinalize()},keySize:4,ivSize:4,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:function(){function e(e){return"string"==typeof e?_:v}return function(t){return{encrypt:function(r,n,i){return e(n).encrypt(t,r,n,i)},decrypt:function(r,n,i){return e(n).decrypt(t,r,n,i)}}}}()}),u=(r.StreamCipher=s.extend({_doFinalize:function(){return this._process(!0)},blockSize:1}),t.mode={}),d=r.BlockCipherMode=n.extend({createEncryptor:function(e,t){return this.Encryptor.create(e,t)},createDecryptor:function(e,t){return this.Decryptor.create(e,t)},init:function(e,t){this._cipher=e,this._iv=t}}),l=u.CBC=function(){var t=d.extend();function r(t,r,n){var i=this._iv;if(i){var c=i;this._iv=e}else c=this._prevBlock;for(var o=0;o<n;o++)t[r+o]^=c[o]}return t.Encryptor=t.extend({processBlock:function(e,t){var n=this._cipher,i=n.blockSize;r.call(this,e,t,i),n.encryptBlock(e,t),this._prevBlock=e.slice(t,t+i)}}),t.Decryptor=t.extend({processBlock:function(e,t){var n=this._cipher,i=n.blockSize,c=e.slice(t,t+i);n.decryptBlock(e,t),r.call(this,e,t,i),this._prevBlock=c}}),t}(),p=(t.pad={}).Pkcs7={pad:function(e,t){for(var r=4*t,n=r-e.sigBytes%r,c=n<<24|n<<16|n<<8|n,o=[],f=0;f<n;f+=4)o.push(c);var a=i.create(o,n);e.concat(a)},unpad:function(e){var t=255&e.words[e.sigBytes-1>>>2];e.sigBytes-=t}},h=(r.BlockCipher=s.extend({cfg:s.cfg.extend({mode:l,padding:p}),reset:function(){s.reset.call(this);var e=this.cfg,t=e.iv,r=e.mode;if(this._xformMode==this._ENC_XFORM_MODE)var n=r.createEncryptor;else{n=r.createDecryptor;this._minBufferSize=1}this._mode&&this._mode.__creator==n?this._mode.init(this,t&&t.words):(this._mode=n.call(r,this,t&&t.words),this._mode.__creator=n)},_doProcessBlock:function(e,t){this._mode.processBlock(e,t)},_doFinalize:function(){var e=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){e.pad(this._data,this.blockSize);var t=this._process(!0)}else{t=this._process(!0);e.unpad(t)}return t},blockSize:4}),r.CipherParams=n.extend({init:function(e){this.mixIn(e)},toString:function(e){return(e||this.formatter).stringify(this)}})),y=(t.format={}).OpenSSL={stringify:function(e){var t=e.ciphertext,r=e.salt;if(r)var n=i.create([1398893684,1701076831]).concat(r).concat(t);else n=t;return n.toString(f)},parse:function(e){var t=f.parse(e),r=t.words;if(1398893684==r[0]&&1701076831==r[1]){var n=i.create(r.slice(2,4));r.splice(0,4),t.sigBytes-=16}return h.create({ciphertext:t,salt:n})}},v=r.SerializableCipher=n.extend({cfg:n.extend({format:y}),encrypt:function(e,t,r,n){n=this.cfg.extend(n);var i=e.createEncryptor(r,n),c=i.finalize(t),o=i.cfg;return h.create({ciphertext:c,key:r,iv:o.iv,algorithm:e,mode:o.mode,padding:o.padding,blockSize:e.blockSize,formatter:n.format})},decrypt:function(e,t,r,n){return n=this.cfg.extend(n),t=this._parse(t,n.format),e.createDecryptor(r,n).finalize(t.ciphertext)},_parse:function(e,t){return"string"==typeof e?t.parse(e,this):e}}),b=(t.kdf={}).OpenSSL={execute:function(e,t,r,n){n||(n=i.random(8));var c=a.create({keySize:t+r}).compute(e,n),o=i.create(c.words.slice(t),4*r);return c.sigBytes=4*t,h.create({key:c,iv:o,salt:n})}},_=r.PasswordBasedCipher=v.extend({cfg:v.cfg.extend({kdf:b}),encrypt:function(e,t,r,n){var i=(n=this.cfg.extend(n)).kdf.execute(r,e.keySize,e.ivSize);n.iv=i.iv;var c=v.encrypt.call(this,e,t,i.key,n);return c.mixIn(i),c},decrypt:function(e,t,r,n){n=this.cfg.extend(n),t=this._parse(t,n.format);var i=n.kdf.execute(r,e.keySize,e.ivSize,t.salt);return n.iv=i.iv,v.decrypt.call(this,e,t,i.key,n)}})}(),CryptoJS.mode.ECB=function(){var e=CryptoJS.lib.BlockCipherMode.extend();return e.Encryptor=e.extend({processBlock:function(e,t){this._cipher.encryptBlock(e,t)}}),e.Decryptor=e.extend({processBlock:function(e,t){this._cipher.decryptBlock(e,t)}}),e}(),function(){var e=CryptoJS,t=e.lib.BlockCipher,r=e.algo,n=[],i=[],c=[],o=[],f=[],a=[],s=[],u=[],d=[],l=[];!function(){for(var e=[],t=0;t<256;t++)e[t]=t<128?t<<1:t<<1^283;var r=0,p=0;for(t=0;t<256;t++){var h=p^p<<1^p<<2^p<<3^p<<4;h=h>>>8^255&h^99,n[r]=h,i[h]=r;var y=e[r],v=e[y],b=e[v],_=257*e[h]^16843008*h;c[r]=_<<24|_>>>8,o[r]=_<<16|_>>>16,f[r]=_<<8|_>>>24,a[r]=_;_=16843009*b^65537*v^257*y^16843008*r;s[h]=_<<24|_>>>8,u[h]=_<<16|_>>>16,d[h]=_<<8|_>>>24,l[h]=_,r?(r=y^e[e[e[b^y]]],p^=e[e[p]]):r=p=1}}();var p=[0,1,2,4,8,16,32,64,128,27,54],h=r.AES=t.extend({_doReset:function(){if(!this._nRounds||this._keyPriorReset!==this._key){for(var e=this._keyPriorReset=this._key,t=e.words,r=e.sigBytes/4,i=4*((this._nRounds=r+6)+1),c=this._keySchedule=[],o=0;o<i;o++)if(o<r)c[o]=t[o];else{var f=c[o-1];o%r?r>6&&o%r==4&&(f=n[f>>>24]<<24|n[f>>>16&255]<<16|n[f>>>8&255]<<8|n[255&f]):(f=n[(f=f<<8|f>>>24)>>>24]<<24|n[f>>>16&255]<<16|n[f>>>8&255]<<8|n[255&f],f^=p[o/r|0]<<24),c[o]=c[o-r]^f}for(var a=this._invKeySchedule=[],h=0;h<i;h++){o=i-h;if(h%4)f=c[o];else f=c[o-4];a[h]=h<4||o<=4?f:s[n[f>>>24]]^u[n[f>>>16&255]]^d[n[f>>>8&255]]^l[n[255&f]]}}},encryptBlock:function(e,t){this._doCryptBlock(e,t,this._keySchedule,c,o,f,a,n)},decryptBlock:function(e,t){var r=e[t+1];e[t+1]=e[t+3],e[t+3]=r,this._doCryptBlock(e,t,this._invKeySchedule,s,u,d,l,i);r=e[t+1];e[t+1]=e[t+3],e[t+3]=r},_doCryptBlock:function(e,t,r,n,i,c,o,f){for(var a=this._nRounds,s=e[t]^r[0],u=e[t+1]^r[1],d=e[t+2]^r[2],l=e[t+3]^r[3],p=4,h=1;h<a;h++){var y=n[s>>>24]^i[u>>>16&255]^c[d>>>8&255]^o[255&l]^r[p++],v=n[u>>>24]^i[d>>>16&255]^c[l>>>8&255]^o[255&s]^r[p++],b=n[d>>>24]^i[l>>>16&255]^c[s>>>8&255]^o[255&u]^r[p++],_=n[l>>>24]^i[s>>>16&255]^c[u>>>8&255]^o[255&d]^r[p++];s=y,u=v,d=b,l=_}y=(f[s>>>24]<<24|f[u>>>16&255]<<16|f[d>>>8&255]<<8|f[255&l])^r[p++],v=(f[u>>>24]<<24|f[d>>>16&255]<<16|f[l>>>8&255]<<8|f[255&s])^r[p++],b=(f[d>>>24]<<24|f[l>>>16&255]<<16|f[s>>>8&255]<<8|f[255&u])^r[p++],_=(f[l>>>24]<<24|f[s>>>16&255]<<16|f[u>>>8&255]<<8|f[255&d])^r[p++];e[t]=y,e[t+1]=v,e[t+2]=b,e[t+3]=_},keySize:8});e.AES=t._createHelper(h)}();var a,i={};i.CryptoJS=CryptoJS,window._$jsvmprt=function(e,t,r){function n(e,t,r){return(n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(e){return!1}}()?Reflect.construct:function(e,t,r){var n=[null];n.push.apply(n,t);var i=new(Function.bind.apply(e,n));return r&&function(e,t){(Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}(i,r.prototype),i}).apply(null,arguments)}function i(e){return function(e){if(Array.isArray(e)){for(var t=0,r=new Array(e.length);t<e.length;t++)r[t]=e[t];return r}}(e)||function(e){if(Symbol.iterator in Object(e)||"[object Arguments]"===Object.prototype.toString.call(e))return Array.from(e)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance")}()}for(var c=[],o=0,f=[],a=0,s=function(e,t){var r=e[t++],n=e[t],i=parseInt(""+r+n,16);if(i>>7==0)return[1,i];if(i>>6==2){var c=parseInt(""+e[++t]+e[++t],16);return i&=63,[2,c=(i<<=8)+c]}if(i>>6==3){var o=parseInt(""+e[++t]+e[++t],16),f=parseInt(""+e[++t]+e[++t],16);return i&=63,[3,f=(i<<=16)+(o<<=8)+f]}},u=function(e,t){var r=parseInt(""+e[t]+e[t+1],16);return r>127?-256+r:r},d=function(e,t){var r=parseInt(""+e[t]+e[t+1]+e[t+2]+e[t+3],16);return r>32767?-65536+r:r},l=function(e,t){var r=parseInt(""+e[t]+e[t+1]+e[t+2]+e[t+3]+e[t+4]+e[t+5]+e[t+6]+e[t+7],16);return r>2147483647?0+r:r},p=function(e,t){return parseInt(""+e[t]+e[t+1],16)},h=function(e,t){return parseInt(""+e[t]+e[t+1]+e[t+2]+e[t+3],16)},y=y||this||window,v=(Object.keys,e.length,0),b="",_=v;_<v+16;_++){var g=""+e[_++]+e[_];g=parseInt(g,16),b+=String.fromCharCode(g)}if("HNOJ@?RC"!=b)throw new Error("error magic number "+b);v+=16,parseInt(""+e[v]+e[v+1],16),v+=8,o=0;for(var m=0;m<4;m++){var S=v+2*m,k=""+e[S++]+e[S],C=parseInt(k,16);o+=(3&C)<<2*m}v+=16,v+=8;var B=parseInt(""+e[v]+e[v+1]+e[v+2]+e[v+3]+e[v+4]+e[v+5]+e[v+6]+e[v+7],16),x=B,w=v+=8,z=h(e,v+=B);z[1],v+=4,c={p:[],q:[]};for(var E=0;E<z;E++){for(var O=s(e,v),I=v+=2*O[0],R=c.p.length,A=0;A<O[1];A++){var M=s(e,I);c.p.push(M[1]),I+=2*M[0]}v=I,c.q.push([R,c.p.length])}var D={5:1,6:1,70:1,22:1,23:1,37:1,73:1},P={72:1},q={74:1},F={11:1,12:1,24:1,26:1,27:1,31:1},j={10:1},J={2:1,29:1,30:1,20:1},H=[],$=[];function X(e,t,r){for(var n=t;n<t+r;){var i=p(e,n);H[n]=i,n+=2,P[i]?($[n]=u(e,n),n+=2):D[i]?($[n]=d(e,n),n+=4):q[i]?($[n]=l(e,n),n+=8):F[i]?($[n]=p(e,n),n+=2):(j[i]||J[i])&&($[n]=h(e,n),n+=4)}}return U(e,w,x/2,[],t,r);function N(e,t,r,s,l,v,b,_){null==v&&(v=this);var g,m,S,k=[],C=0;b&&(g=b);var B,x,w=t,z=w+2*r;if(!_)for(;w<z;){var E=parseInt(""+e[w]+e[w+1],16);w+=2;var O=3&(B=13*E%241);if(B>>=2,O>2)O=3&B,B>>=2,O<1?(O=B)<4?(g=k[C--],k[C]=k[C]-g):O<6?(g=k[C--],k[C]=k[C]===g):O<15&&(g=k[C],k[C]=k[C-1],k[C-1]=g):O<2?(O=B)<5&&(x=p(e,w),w+=2,g=l[x],k[++C]=g):O<3?(O=B)<6||(O<8?g=k[C--]:O<12&&(x=d(e,w),f[++a]=[[w+4,x-3],0,0],w+=2*x-2)):(O=B)<2?(g=k[C--],k[C]=k[C]<g):O<9&&(x=p(e,w),w+=2,k[C]=k[C][x]);else if(O>1)if(O=3&B,B>>=2,O>2)(O=B)>5?(x=p(e,w),w+=2,k[++C]=l["$"+x]):O>3&&(x=d(e,w),f[a][0]&&!f[a][2]?f[a][1]=[w+4,x-3]:f[a++]=[0,[w+4,x-3],0],w+=2*x-2);else if(O>1){if((O=B)>2)if(k[C--])w+=4;else{if((x=d(e,w))<0){_=1,X(e,t,2*r),w+=2*x-2;break}w+=2*x-2}else if(O>0){for(x=h(e,w),g="",A=c.q[x][0];A<c.q[x][1];A++)g+=String.fromCharCode(o^c.p[A]);k[++C]=g,w+=4}}else O>0?(O=B)>1?(g=k[C--],k[C]=k[C]+g):O>-1&&(k[++C]=y):(O=B)>9?(x=p(e,w),w+=2,g=k[C--],l[x]=g):O>7?(x=h(e,w),w+=4,m=C+1,k[C-=x-1]=x?k.slice(C,m):[]):O>0&&(g=k[C--],k[C]=k[C]>g);else if(O>0){if(O=3&B,B>>=2,O<1){if((O=B)>9);else if(O>5)x=p(e,w),w+=2,k[C-=x]=0===x?new k[C]:n(k[C],i(k.slice(C+1,C+x+1)));else if(O>3){x=d(e,w);try{if(f[a][2]=1,1==(g=N(e,w+4,x-3,[],l,v,null,0))[0])return g}catch(b){if(f[a]&&f[a][1]&&1==(g=N(e,f[a][1][0],f[a][1][1],[],l,v,b,0))[0])return g}finally{if(f[a]&&f[a][0]&&1==(g=N(e,f[a][0][0],f[a][0][1],[],l,v,null,0))[0])return g;f[a]=0,a--}w+=2*x-2}}else if(O<2){if((O=B)>12)k[++C]=u(e,w),w+=2;else if(O>8){for(x=h(e,w),O="",A=c.q[x][0];A<c.q[x][1];A++)O+=String.fromCharCode(o^c.p[A]);w+=4,k[C]=k[C][O]}}else if(O<3)(O=B)>11?(g=k[C],k[++C]=g):O>0&&(k[++C]=g);else if((O=B)<1)k[C]=!k[C];else if(O<3){if((x=d(e,w))<0){_=1,X(e,t,2*r),w+=2*x-2;break}w+=2*x-2}}else if(O=3&B,B>>=2,O>2)(O=B)<1&&(k[++C]=null);else if(O>1){if((O=B)<9){for(g=k[C--],x=h(e,w),O="",A=c.q[x][0];A<c.q[x][1];A++)O+=String.fromCharCode(o^c.p[A]);w+=4,k[C--][O]=g}}else if(O>0)(O=B)<4?(m=k[C--],(O=k[C]).x===N?O.y>=1?k[C]=U(e,O.c,O.l,[m],O.z,S,null,1):(k[C]=U(e,O.c,O.l,[m],O.z,S,null,0),O.y++):k[C]=O(m)):O<6&&(k[C-=1]=k[C][k[C+1]]);else{if((O=B)<1)return[1,k[C--]];O<14?(m=k[C--],S=k[C--],(O=k[C--]).x===N?O.y>=1?k[++C]=U(e,O.c,O.l,m,O.z,S,null,1):(k[++C]=U(e,O.c,O.l,m,O.z,S,null,0),O.y++):k[++C]=O.apply(S,m)):O<16&&(x=d(e,w),(I=function t(){var r=arguments;return t.y>0||t.y++,U(e,t.c,t.l,r,t.z,this,null,0)}).c=w+4,I.l=x-2,I.x=N,I.y=0,I.z=l,k[C]=I,w+=2*x-2)}}if(_)for(;w<z;)if(E=H[w],w+=2,O=3&(B=13*E%241),B>>=2,O<1)if(O=3&B,B>>=2,O>2)(O=B)<1&&(k[++C]=null);else if(O>1){if((O=B)<9){for(g=k[C--],x=$[w],O="",A=c.q[x][0];A<c.q[x][1];A++)O+=String.fromCharCode(o^c.p[A]);w+=4,k[C--][O]=g}}else if(O>0)(O=B)<4?(m=k[C--],(O=k[C]).x===N?O.y>=1?k[C]=U(e,O.c,O.l,[m],O.z,S,null,1):(k[C]=U(e,O.c,O.l,[m],O.z,S,null,0),O.y++):k[C]=O(m)):O<6&&(k[C-=1]=k[C][k[C+1]]);else{var I;if((O=B)>14)x=$[w],(I=function t(){var r=arguments;return t.y>0||t.y++,U(e,t.c,t.l,r,t.z,this,null,0)}).c=w+4,I.l=x-2,I.x=N,I.y=0,I.z=l,k[C]=I,w+=2*x-2;else if(O>12)m=k[C--],S=k[C--],(O=k[C--]).x===N?O.y>=1?k[++C]=U(e,O.c,O.l,m,O.z,S,null,1):(k[++C]=U(e,O.c,O.l,m,O.z,S,null,0),O.y++):k[++C]=O.apply(S,m);else if(O>-1)return[1,k[C--]]}else if(O<2)if(O=3&B,B>>=2,O>2)(O=B)<1?k[C]=!k[C]:O<3&&(w+=2*(x=$[w])-2);else if(O>1)(O=B)<2?k[++C]=g:O<13&&(g=k[C],k[++C]=g);else if(O>0)if((O=B)<10){for(x=$[w],O="",A=c.q[x][0];A<c.q[x][1];A++)O+=String.fromCharCode(o^c.p[A]);w+=4,k[C]=k[C][O]}else O<14&&(k[++C]=$[w],w+=2);else if((O=B)<5){x=$[w];try{if(f[a][2]=1,1==(g=N(e,w+4,x-3,[],l,v,null,0))[0])return g}catch(b){if(f[a]&&f[a][1]&&1==(g=N(e,f[a][1][0],f[a][1][1],[],l,v,b,0))[0])return g}finally{if(f[a]&&f[a][0]&&1==(g=N(e,f[a][0][0],f[a][0][1],[],l,v,null,0))[0])return g;f[a]=0,a--}w+=2*x-2}else O<7&&(x=$[w],w+=2,k[C-=x]=0===x?new k[C]:n(k[C],i(k.slice(C+1,C+x+1))));else if(O<3)if(O=3&B,B>>=2,O<1)(O=B)>9?(x=$[w],w+=2,g=k[C--],l[x]=g):O>7?(x=$[w],w+=4,m=C+1,k[C-=x-1]=x?k.slice(C,m):[]):O>0&&(g=k[C--],k[C]=k[C]>g);else if(O<2)(O=B)>1?(g=k[C--],k[C]=k[C]+g):O>-1&&(k[++C]=y);else if(O<3)if((O=B)<2){for(x=$[w],g="",A=c.q[x][0];A<c.q[x][1];A++)g+=String.fromCharCode(o^c.p[A]);k[++C]=g,w+=4}else O<4&&(k[C--]?w+=4:w+=2*(x=$[w])-2);else(O=B)>5?(x=$[w],w+=2,k[++C]=l["$"+x]):O>3&&(x=$[w],f[a][0]&&!f[a][2]?f[a][1]=[w+4,x-3]:f[a++]=[0,[w+4,x-3],0],w+=2*x-2);else O=3&B,B>>=2,O<1?(O=B)<4?(g=k[C--],k[C]=k[C]-g):O<6?(g=k[C--],k[C]=k[C]===g):O<15&&(g=k[C],k[C]=k[C-1],k[C-1]=g):O<2?(O=B)<5&&(x=$[w],w+=2,g=l[x],k[++C]=g):O<3?(O=B)>10?(x=$[w],f[++a]=[[w+4,x-3],0,0],w+=2*x-2):O>6&&(g=k[C--]):(O=B)<2?(g=k[C--],k[C]=k[C]<g):O<9&&(x=$[w],w+=2,k[C]=k[C][x]);return[0,null]}function U(e,t,r,n,i,c,o,f){var a,s;null==c&&(c=this),i&&!i.d&&(i.d=0,i.$0=i,i[1]={});var u={},d=u.d=i?i.d+1:0;for(u["$"+d]=u,s=0;s<d;s++)u[a="$"+s]=i[a];for(s=0,d=u.length=n.length;s<d;s++)u[s]=n[s];return f&&!H[t]&&X(e,t,2*r),H[t]?N(e,t,r,0,u,c,null,1)[1]:N(e,t,r,0,u,c,null,0)[1]}},a=[i,,"undefined"!=typeof sessionStorage?sessionStorage:void 0,"undefined"!=typeof console?console:void 0,"undefined"!=typeof document?document:void 0,"undefined"!=typeof navigator?navigator:void 0,"undefined"!=typeof screen?screen:void 0,"undefined"!=typeof Intl?Intl:void 0,"undefined"!=typeof Array?Array:void 0,"undefined"!=typeof Object?Object:void 0],window._$jsvmprt("484e4f4a403f524300332d0511788d78e08713dc000000000000080a1b000b001e00011f0002000025003d46000306001a271f0c1b000b03221e0002240200030a0001101c18010005001c1b000b02221e00042418000a00011022011700061c18010007001f010200051f020200061f030200071f040200002500121b010b011b010b03041b010b043e001f050200002501981b000b041e0008221e000924131e000a02000b0200001a020a0001101f061800220117000a1c131e000c1a001f07460003060006271f2c050157131e000c1a002202000d1d000e2202000f1d00102218041d00112218071e00121d00132218071e00141d00152218071e0016220117000a1c131e000c1a001e001522011700071c0200001d00172218071e00181d0019221b000b041e001a1d001b221b010b011b010b02041d001c221b000b051e001d1d001e221b000b061e001f1d0020221b000b061e00211d0022221b000b051e00231d0024221b000b051e00251d0026221b000b051e00271d0028221b000b051e00291d002a221b000b051e002b1d002c22180622011700071c0200004801191d002d2218071e002e1d002f221b000b07221e0030240a000010221e0031240a0000101e00321d00332218011d00342218021d00352213221e0036240200370a0001101e00381d003922131e003a1e003b1d003c1f081b010b05260a00001017000b180802003d1d003e1b000b051e003f17000a180818031d004018080007131e000c1a00001f0602000025007f131e000c1a00221b000b051e001d1d001e221b000b061e001f1d0020221b000b061e00211d0022221b000b051e00231d0024221b000b051e00251d0026221b000b051e00271d0028221b000b051e00291d002a221b000b051e002b1d002c221b000b07221e0030240a000010221e0031240a0000101e00321d0033001f070200002501520200411f060a00001f0702000025005d1800221e0042240a0000101f0618061e003b1f07180718013a1700251b000b0818011807294801281a01221e0043240200440a0001101806281f0616001c18071801391700141806221e004524480018010a0002101f061806001f080200002500731b020b0826180148100a0002101f061b010b001e00461e0047221e00482418060a0001101f071b010b001e0049221e004a2418001807131e000c1a002218071d004b221b010b001e004c1e004d1d004c221b010b001e004e1e004f1d00500a0003101f081808221e0042240a000010001f091b000b09221e00512418000a000110221e0052240200002500241800020053281b020b00180019281f061b020b07221e00542418060a0001101c000a0001101c1807221e0054240200550a0001101c1809261807221e0043240200560a00011018060a0002101f0a180a001f081b000b0118061d00571b000b0118071d00581b000b0118081d005900005a000852636861657e5b42046670637f1962746262787e7f42657e6370767431767465317770787d7475077674655865747c166674737061613c62746262787e7f3c637477746374630c747f6574634e7c7465797e750970767447746378776806727e7e7a7874057c706572790643747654696110624e674e6674734e78752c394d663a38065e737b7472650420282929037078750a65787a657e7a4e667473087061614e7f707c740f7574677872744e617d7065777e637c0435667875097574677872744e78750735637476787e7f06637476787e7f0535646274630f6163787e637865684e637476787e7f03357e62027e6208637477746363746307637477746374630c637e7e654e637477746374630d727e7e7a7874547f70737d74750e727e7e7a78744e747f70737d74750566787565790c62726374747f4e6678756579067974787679650d62726374747f4e797478767965087d707f76647076741073637e666274634e7d707f766470767408617d7065777e637c1073637e666274634e617d7065777e637c0b706161527e75745f707c740c73637e666274634e7f707c740a70616147746362787e7f0f73637e666274634e67746362787e7f067e7f5d787f740e73637e666274634e7e7f7d787f7408677463787768576109357d707f76647076740c7061614e7d707f76647076740e5570657445787c74577e637c70650f6374627e7d6774755e6165787e7f620865787c744b7e7f740d65787c746b7e7f744e7f707c740f78624e617076744e67786278737d740b777e7264624e62657065740a7c706572795c747578701a39757862617d70683c7c7e75742b3177647d7d62726374747f38077c7065727974620d78624e77647d7d62726374747f07797862657e6368067d747f7665790b797862657e63684e7d747f04202524281962747264637865684e677463787778727065787e7f4e7078750a767465537065657463680c737065657463684e787f777e12667473706161203f213a232123202127232908657e426563787f76047b7e787f012105627d78727403747f7204446577290561706362740350544207747f7263686165027867047c7e7574035253520361707505417a7262260761707575787f76047a74686207777e6354707279012c04616462790f78624e747f7263686165787e7f2c2001370f767465527e7c7c7e7f417063707c6211767465547062684378627a417063707c620d747f7263686165417063707c62",a);var c=a[1],s=c.getCommonParams,u=c.getEasyRiskParams,l=c.encryptParams;window.genXTTParams=function(e){return l(e)};"""
-
- -
- - - -
-
-
#   - - - def - get_acrawler(): -
- -
- View Source -
def get_acrawler():
-    return """Function(function(t){return'w={S(S,K){if(!a[S]){a[S]={};for(y=0;y<S;y)a[S][S.charAt(y)]=y}a[S][K]}K=String.fromCharCode,a={},y={x:(K){null==K?"":""==K?null:y.y(K,32,(a){S("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",K.charAt(a})},y:(S,a,y){p,m,o,T,l,r,k,i=[],J=4,q=4,j=3,I="",b=[],z={val:y(0),position:a,index:1};for(p=0;p<3;p+=1)i[p]=p;for(o=0,l=Math.pow(2,2),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;switch(o){case 0:for(o=0,l=Math.pow(2,8),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;k=K(o)1:for(o=0,l=Math.pow(2,16),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;k=K(o)2:""}for(i[3]=k,m=k,b.push(k);;){if(z.index>S)"";for(o=0,l=Math.pow(2,j),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;switch(k=o){case 0:for(o=0,l=Math.pow(2,8),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;i[q]=K(o),k=q-1,J--1:for(o=0,l=Math.pow(2,16),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;i[q]=K(o),k=q-1,J--2:b.join("")}if(0==J&&(J=Math.pow(2,j),j),i[k])I=i[k]if(k!==q)null;I=m+m.charAt(0)}b.push(I),i[q]=m+I.charAt(0),m=I,0==--J&&(J=Math.pow(2,j),j)}}};y};""==typeof define&&define.amd?define({w}):"undefined"!=typeof module&&null!=module?module.exports=w:"undefined"!=typeof angular&&null!=angular&&angular.module("w",[]).factory("w",{w}),eval(w.x("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";'.replace(/[-]/g,function(m){return t[m.charCodeAt(0)&15]})}("var function ().length++return ));break;case ;else{".split("")))();"""
-
- -
- - - -
-
- - - - \ No newline at end of file diff --git a/docs/TikTokApi/browser_utilities/stealth.html b/docs/TikTokApi/browser_utilities/stealth.html deleted file mode 100644 index 8e5039a7..00000000 --- a/docs/TikTokApi/browser_utilities/stealth.html +++ /dev/null @@ -1,1467 +0,0 @@ - - - - - - - TikTokApi.browser_utilities.stealth API documentation - - - - - - - - -
-
-

-TikTokApi.browser_utilities.stealth

- - -
- View Source -
import re
-
-
-def chrome_runtime(page) -> None:
-    page.evaluateOnNewDocument(
-        """
-() => {
-    window.chrome = {
-        runtime: {}
-    }
-}
-"""
-    )
-
-
-def console_debug(page) -> None:
-    page.evaluateOnNewDocument(
-        """
-() => {
-    window.console.debug = () => {
-        return null
-    }
-}
-"""
-    )
-
-
-def iframe_content_window(page) -> None:
-    page.evaluateOnNewDocument(
-        """
-() => {
-  try {
-    // Adds a contentWindow proxy to the provided iframe element
-    const addContentWindowProxy = iframe => {
-      const contentWindowProxy = {
-        get(target, key) {
-          // Now to the interesting part:
-          // We actually make this thing behave like a regular iframe window,
-          // by intercepting calls to e.g. `.self` and redirect it to the correct thing. :)
-          // That makes it possible for these assertions to be correct:
-          // iframe.contentWindow.self === window.top // must be false
-          if (key === 'self') {
-            return this
-          }
-          // iframe.contentWindow.frameElement === iframe // must be true
-          if (key === 'frameElement') {
-            return iframe
-          }
-          return Reflect.get(target, key)
-        }
-      }
-      if (!iframe.contentWindow) {
-        const proxy = new Proxy(window, contentWindowProxy)
-        Object.defineProperty(iframe, 'contentWindow', {
-          get() {
-            return proxy
-          },
-          set(newValue) {
-            return newValue // contentWindow is immutable
-          },
-          enumerable: true,
-          configurable: false
-        })
-      }
-    }
-    // Handles iframe element creation, augments `srcdoc` property so we can intercept further
-    const handleIframeCreation = (target, thisArg, args) => {
-      const iframe = target.apply(thisArg, args)
-      // We need to keep the originals around
-      const _iframe = iframe
-      const _srcdoc = _iframe.srcdoc
-      // Add hook for the srcdoc property
-      // We need to be very surgical here to not break other iframes by accident
-      Object.defineProperty(iframe, 'srcdoc', {
-        configurable: true, // Important, so we can reset this later
-        get: function() {
-          return _iframe.srcdoc
-        },
-        set: function(newValue) {
-          addContentWindowProxy(this)
-          // Reset property, the hook is only needed once
-          Object.defineProperty(iframe, 'srcdoc', {
-            configurable: false,
-            writable: false,
-            value: _srcdoc
-          })
-          _iframe.srcdoc = newValue
-        }
-      })
-      return iframe
-    }
-    // Adds a hook to intercept iframe creation events
-    const addIframeCreationSniffer = () => {
-      /* global document */
-      const createElement = {
-        // Make toString() native
-        get(target, key) {
-          return Reflect.get(target, key)
-        },
-        apply: function(target, thisArg, args) {
-          const isIframe =
-            args && args.length && `${args[0]}`.toLowerCase() === 'iframe'
-          if (!isIframe) {
-            // Everything as usual
-            return target.apply(thisArg, args)
-          } else {
-            return handleIframeCreation(target, thisArg, args)
-          }
-        }
-      }
-      // All this just due to iframes with srcdoc bug
-      document.createElement = new Proxy(
-        document.createElement,
-        createElement
-      )
-    }
-    // Let's go
-    addIframeCreationSniffer()
-  } catch (err) {
-    // console.warn(err)
-  }
-}
-"""
-    )
-
-
-def media_codecs(page) -> None:
-    page.evaluateOnNewDocument(
-        """
-    () => {
-  try {
-    /**
-     * Input might look funky, we need to normalize it so e.g. whitespace isn't an issue for our spoofing.
-     *
-     * @example
-     * video/webm; codecs="vp8, vorbis"
-     * video/mp4; codecs="avc1.42E01E"
-     * audio/x-m4a;
-     * audio/ogg; codecs="vorbis"
-     * @param {String} arg
-     */
-    const parseInput = arg => {
-      const [mime, codecStr] = arg.trim().split(';')
-      let codecs = []
-      if (codecStr && codecStr.includes('codecs="')) {
-        codecs = codecStr
-          .trim()
-          .replace(`codecs="`, '')
-          .replace(`"`, '')
-          .trim()
-          .split(',')
-          .filter(x => !!x)
-          .map(x => x.trim())
-      }
-      return { mime, codecStr, codecs }
-    }
-    /* global HTMLMediaElement */
-    const canPlayType = {
-      // Make toString() native
-      get(target, key) {
-        // Mitigate Chromium bug (#130)
-        if (typeof target[key] === 'function') {
-          return target[key].bind(target)
-        }
-        return Reflect.get(target, key)
-      },
-      // Intercept certain requests
-      apply: function(target, ctx, args) {
-        if (!args || !args.length) {
-          return target.apply(ctx, args)
-        }
-        const { mime, codecs } = parseInput(args[0])
-        // This specific mp4 codec is missing in Chromium
-        if (mime === 'video/mp4') {
-          if (codecs.includes('avc1.42E01E')) {
-            return 'probably'
-          }
-        }
-        // This mimetype is only supported if no codecs are specified
-        if (mime === 'audio/x-m4a' && !codecs.length) {
-          return 'maybe'
-        }
-        // This mimetype is only supported if no codecs are specified
-        if (mime === 'audio/aac' && !codecs.length) {
-          return 'probably'
-        }
-        // Everything else as usual
-        return target.apply(ctx, args)
-      }
-    }
-    HTMLMediaElement.prototype.canPlayType = new Proxy(
-      HTMLMediaElement.prototype.canPlayType,
-      canPlayType
-    )
-  } catch (err) {}
-}
-"""
-    )
-
-
-def navigator_languages(page) -> None:
-    page.evaluateOnNewDocument(
-        """
-() => {
-    Object.defineProperty(navigator, 'languages', {
-        get: () => ['en-US', 'en']
-    })
-}
-    """
-    )
-
-
-def navigator_permissions(page) -> None:
-    page.evaluateOnNewDocument(
-        """
-() => {
-    const originalQuery = window.navigator.permissions.query
-    window.navigator.permissions.__proto__.query = parameters =>
-        parameters.name === 'notifications'
-            ? Promise.resolve({ state: Notification.permission })
-            : originalQuery(parameters)
-    const oldCall = Function.prototype.call
-    function call () {
-        return oldCall.apply(this, arguments)
-    }
-    Function.prototype.call = call
-    const nativeToStringFunctionString = Error.toString().replace(
-        /Error/g,
-        'toString'
-    )
-    const oldToString = Function.prototype.toString
-    function functionToString () {
-        if (this === window.navigator.permissions.query) {
-            return 'function query() { [native code] }'
-        }
-        if (this === functionToString) {
-            return nativeToStringFunctionString
-        }
-        return oldCall.call(oldToString, this)
-    }
-    Function.prototype.toString = functionToString
-}
-    """
-    )
-
-
-def navigator_plugins(page) -> None:
-    page.evaluateOnNewDocument(
-        """
-() => {
-    function mockPluginsAndMimeTypes() {
-        const makeFnsNative = (fns = []) => {
-            const oldCall = Function.prototype.call
-            function call() {
-                return oldCall.apply(this, arguments)
-            }
-            Function.prototype.call = call
-            const nativeToStringFunctionString = Error.toString().replace(
-                /Error/g,
-                'toString'
-            )
-            const oldToString = Function.prototype.toString
-            function functionToString() {
-                for (const fn of fns) {
-                    if (this === fn.ref) {
-                        return `function ${fn.name}() { [native code] }`
-                    }
-                }
-                if (this === functionToString) {
-                    return nativeToStringFunctionString
-                }
-                return oldCall.call(oldToString, this)
-            }
-            Function.prototype.toString = functionToString
-        }
-        const mockedFns = []
-        const fakeData = {
-            mimeTypes: [
-                {
-                    type: 'application/pdf',
-                    suffixes: 'pdf',
-                    description: '',
-                    __pluginName: 'Chrome PDF Viewer'
-                },
-                {
-                    type: 'application/x-google-chrome-pdf',
-                    suffixes: 'pdf',
-                    description: 'Portable Document Format',
-                    __pluginName: 'Chrome PDF Plugin'
-                },
-                {
-                    type: 'application/x-nacl',
-                    suffixes: '',
-                    description: 'Native Client Executable',
-                    enabledPlugin: Plugin,
-                    __pluginName: 'Native Client'
-                },
-                {
-                    type: 'application/x-pnacl',
-                    suffixes: '',
-                    description: 'Portable Native Client Executable',
-                    __pluginName: 'Native Client'
-                }
-            ],
-            plugins: [
-                {
-                    name: 'Chrome PDF Plugin',
-                    filename: 'internal-pdf-viewer',
-                    description: 'Portable Document Format'
-                },
-                {
-                    name: 'Chrome PDF Viewer',
-                    filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai',
-                    description: ''
-                },
-                {
-                    name: 'Native Client',
-                    filename: 'internal-nacl-plugin',
-                    description: ''
-                }
-            ],
-            fns: {
-                namedItem: instanceName => {
-                    const fn = function (name) {
-                        if (!arguments.length) {
-                            throw new TypeError(
-                                `Failed to execute 'namedItem' on '${instanceName}': 1 argument required, but only 0 present.`
-                            )
-                        }
-                        return this[name] || null
-                    }
-                    mockedFns.push({ ref: fn, name: 'namedItem' })
-                    return fn
-                },
-                item: instanceName => {
-                    const fn = function (index) {
-                        if (!arguments.length) {
-                            throw new TypeError(
-                                `Failed to execute 'namedItem' on '${instanceName}': 1 argument required, but only 0 present.`
-                            )
-                        }
-                        return this[index] || null
-                    }
-                    mockedFns.push({ ref: fn, name: 'item' })
-                    return fn
-                },
-                refresh: instanceName => {
-                    const fn = function () {
-                        return undefined
-                    }
-                    mockedFns.push({ ref: fn, name: 'refresh' })
-                    return fn
-                }
-            }
-        }
-        const getSubset = (keys, obj) =>
-            keys.reduce((a, c) => ({ ...a, [c]: obj[c] }), {})
-        function generateMimeTypeArray() {
-            const arr = fakeData.mimeTypes
-                .map(obj => getSubset(['type', 'suffixes', 'description'], obj))
-                .map(obj => Object.setPrototypeOf(obj, MimeType.prototype))
-            arr.forEach(obj => {
-                arr[obj.type] = obj
-            })
-            arr.namedItem = fakeData.fns.namedItem('MimeTypeArray')
-            arr.item = fakeData.fns.item('MimeTypeArray')
-            return Object.setPrototypeOf(arr, MimeTypeArray.prototype)
-        }
-        const mimeTypeArray = generateMimeTypeArray()
-        Object.defineProperty(navigator, 'mimeTypes', {
-            get: () => mimeTypeArray
-        })
-        function generatePluginArray() {
-            const arr = fakeData.plugins
-                .map(obj => getSubset(['name', 'filename', 'description'], obj))
-                .map(obj => {
-                    const mimes = fakeData.mimeTypes.filter(
-                        m => m.__pluginName === obj.name
-                    )
-                    mimes.forEach((mime, index) => {
-                        navigator.mimeTypes[mime.type].enabledPlugin = obj
-                        obj[mime.type] = navigator.mimeTypes[mime.type]
-                        obj[index] = navigator.mimeTypes[mime.type]
-                    })
-                    obj.length = mimes.length
-                    return obj
-                })
-                .map(obj => {
-                    obj.namedItem = fakeData.fns.namedItem('Plugin')
-                    obj.item = fakeData.fns.item('Plugin')
-                    return obj
-                })
-                .map(obj => Object.setPrototypeOf(obj, Plugin.prototype))
-            arr.forEach(obj => {
-                arr[obj.name] = obj
-            })
-            arr.namedItem = fakeData.fns.namedItem('PluginArray')
-            arr.item = fakeData.fns.item('PluginArray')
-            arr.refresh = fakeData.fns.refresh('PluginArray')
-            return Object.setPrototypeOf(arr, PluginArray.prototype)
-        }
-        const pluginArray = generatePluginArray()
-        Object.defineProperty(navigator, 'plugins', {
-            get: () => pluginArray
-        })
-        makeFnsNative(mockedFns)
-    }
-    try {
-        const isPluginArray = navigator.plugins instanceof PluginArray
-        const hasPlugins = isPluginArray && navigator.plugins.length > 0
-        if (isPluginArray && hasPlugins) {
-            return
-        }
-        mockPluginsAndMimeTypes()
-    } catch (err) { }
-}
-"""
-    )
-
-
-def navigator_webdriver(page) -> None:
-    page.evaluateOnNewDocument(
-        """
-() => {
-    Object.defineProperty(window, 'navigator', {
-    value: new Proxy(navigator, {
-      has: (target, key) => (key === 'webdriver' ? false : key in target),
-      get: (target, key) =>
-        key === 'webdriver'
-          ? undefined
-          : typeof target[key] === 'function'
-          ? target[key].bind(target)
-          : target[key]
-    })
-  })
-}
-    """
-    )
-
-
-def user_agent(page) -> None:
-    return
-    ua = page.browser.userAgent()
-    ua = ua.replace("HeadlessChrome", "Chrome")  # hide headless nature
-    ua = re.sub(
-        r"\(([^)]+)\)", "(Windows NT 10.0; Win64; x64)", ua, 1
-    )  # ensure windows
-
-    page.setUserAgent(ua)
-
-
-def webgl_vendor(page) -> None:
-    page.evaluateOnNewDocument(
-        """
-() => {
-    try {
-        const getParameter = WebGLRenderingContext.prototype.getParameter
-        WebGLRenderingContext.prototype.getParameter = function (parameter) {
-          if (parameter === 37445) {
-            return 'Intel Inc.'
-          }
-          if (parameter === 37446) {
-            return 'Intel Iris OpenGL Engine'
-          }
-          return getParameter.apply(this, [parameter])
-        }
-      } catch (err) {}
-}
-"""
-    )
-
-
-def window_outerdimensions(page) -> None:
-    page.evaluateOnNewDocument(
-        """
-() => {
-    try {
-        if (window.outerWidth && window.outerHeight) {
-            return
-        }
-        const windowFrame = 85
-        window.outerWidth = window.innerWidth
-        window.outerHeight = window.innerHeight + windowFrame
-    } catch (err) { }
-}
-"""
-    )
-
-
-def stealth(page) -> None:
-    # chrome_runtime(page)
-    console_debug(page)
-    iframe_content_window(page)
-    # navigator_languages(page)
-    navigator_permissions(page)
-    navigator_plugins(page)
-    navigator_webdriver(page)
-    # navigator_vendor(page)
-    user_agent(page)
-    webgl_vendor(page)
-    window_outerdimensions(page)
-    media_codecs(page)
-
- -
- -
-
-
#   - - - def - chrome_runtime(page) -> None: -
- -
- View Source -
def chrome_runtime(page) -> None:
-    page.evaluateOnNewDocument(
-        """
-() => {
-    window.chrome = {
-        runtime: {}
-    }
-}
-"""
-    )
-
- -
- - - -
-
-
#   - - - def - console_debug(page) -> None: -
- -
- View Source -
def console_debug(page) -> None:
-    page.evaluateOnNewDocument(
-        """
-() => {
-    window.console.debug = () => {
-        return null
-    }
-}
-"""
-    )
-
- -
- - - -
-
-
#   - - - def - iframe_content_window(page) -> None: -
- -
- View Source -
def iframe_content_window(page) -> None:
-    page.evaluateOnNewDocument(
-        """
-() => {
-  try {
-    // Adds a contentWindow proxy to the provided iframe element
-    const addContentWindowProxy = iframe => {
-      const contentWindowProxy = {
-        get(target, key) {
-          // Now to the interesting part:
-          // We actually make this thing behave like a regular iframe window,
-          // by intercepting calls to e.g. `.self` and redirect it to the correct thing. :)
-          // That makes it possible for these assertions to be correct:
-          // iframe.contentWindow.self === window.top // must be false
-          if (key === 'self') {
-            return this
-          }
-          // iframe.contentWindow.frameElement === iframe // must be true
-          if (key === 'frameElement') {
-            return iframe
-          }
-          return Reflect.get(target, key)
-        }
-      }
-      if (!iframe.contentWindow) {
-        const proxy = new Proxy(window, contentWindowProxy)
-        Object.defineProperty(iframe, 'contentWindow', {
-          get() {
-            return proxy
-          },
-          set(newValue) {
-            return newValue // contentWindow is immutable
-          },
-          enumerable: true,
-          configurable: false
-        })
-      }
-    }
-    // Handles iframe element creation, augments `srcdoc` property so we can intercept further
-    const handleIframeCreation = (target, thisArg, args) => {
-      const iframe = target.apply(thisArg, args)
-      // We need to keep the originals around
-      const _iframe = iframe
-      const _srcdoc = _iframe.srcdoc
-      // Add hook for the srcdoc property
-      // We need to be very surgical here to not break other iframes by accident
-      Object.defineProperty(iframe, 'srcdoc', {
-        configurable: true, // Important, so we can reset this later
-        get: function() {
-          return _iframe.srcdoc
-        },
-        set: function(newValue) {
-          addContentWindowProxy(this)
-          // Reset property, the hook is only needed once
-          Object.defineProperty(iframe, 'srcdoc', {
-            configurable: false,
-            writable: false,
-            value: _srcdoc
-          })
-          _iframe.srcdoc = newValue
-        }
-      })
-      return iframe
-    }
-    // Adds a hook to intercept iframe creation events
-    const addIframeCreationSniffer = () => {
-      /* global document */
-      const createElement = {
-        // Make toString() native
-        get(target, key) {
-          return Reflect.get(target, key)
-        },
-        apply: function(target, thisArg, args) {
-          const isIframe =
-            args && args.length && `${args[0]}`.toLowerCase() === 'iframe'
-          if (!isIframe) {
-            // Everything as usual
-            return target.apply(thisArg, args)
-          } else {
-            return handleIframeCreation(target, thisArg, args)
-          }
-        }
-      }
-      // All this just due to iframes with srcdoc bug
-      document.createElement = new Proxy(
-        document.createElement,
-        createElement
-      )
-    }
-    // Let's go
-    addIframeCreationSniffer()
-  } catch (err) {
-    // console.warn(err)
-  }
-}
-"""
-    )
-
- -
- - - -
-
-
#   - - - def - media_codecs(page) -> None: -
- -
- View Source -
def media_codecs(page) -> None:
-    page.evaluateOnNewDocument(
-        """
-    () => {
-  try {
-    /**
-     * Input might look funky, we need to normalize it so e.g. whitespace isn't an issue for our spoofing.
-     *
-     * @example
-     * video/webm; codecs="vp8, vorbis"
-     * video/mp4; codecs="avc1.42E01E"
-     * audio/x-m4a;
-     * audio/ogg; codecs="vorbis"
-     * @param {String} arg
-     */
-    const parseInput = arg => {
-      const [mime, codecStr] = arg.trim().split(';')
-      let codecs = []
-      if (codecStr && codecStr.includes('codecs="')) {
-        codecs = codecStr
-          .trim()
-          .replace(`codecs="`, '')
-          .replace(`"`, '')
-          .trim()
-          .split(',')
-          .filter(x => !!x)
-          .map(x => x.trim())
-      }
-      return { mime, codecStr, codecs }
-    }
-    /* global HTMLMediaElement */
-    const canPlayType = {
-      // Make toString() native
-      get(target, key) {
-        // Mitigate Chromium bug (#130)
-        if (typeof target[key] === 'function') {
-          return target[key].bind(target)
-        }
-        return Reflect.get(target, key)
-      },
-      // Intercept certain requests
-      apply: function(target, ctx, args) {
-        if (!args || !args.length) {
-          return target.apply(ctx, args)
-        }
-        const { mime, codecs } = parseInput(args[0])
-        // This specific mp4 codec is missing in Chromium
-        if (mime === 'video/mp4') {
-          if (codecs.includes('avc1.42E01E')) {
-            return 'probably'
-          }
-        }
-        // This mimetype is only supported if no codecs are specified
-        if (mime === 'audio/x-m4a' && !codecs.length) {
-          return 'maybe'
-        }
-        // This mimetype is only supported if no codecs are specified
-        if (mime === 'audio/aac' && !codecs.length) {
-          return 'probably'
-        }
-        // Everything else as usual
-        return target.apply(ctx, args)
-      }
-    }
-    HTMLMediaElement.prototype.canPlayType = new Proxy(
-      HTMLMediaElement.prototype.canPlayType,
-      canPlayType
-    )
-  } catch (err) {}
-}
-"""
-    )
-
- -
- - - -
- - - - -
-
#   - - - def - user_agent(page) -> None: -
- -
- View Source -
def user_agent(page) -> None:
-    return
-    ua = page.browser.userAgent()
-    ua = ua.replace("HeadlessChrome", "Chrome")  # hide headless nature
-    ua = re.sub(
-        r"\(([^)]+)\)", "(Windows NT 10.0; Win64; x64)", ua, 1
-    )  # ensure windows
-
-    page.setUserAgent(ua)
-
- -
- - - -
-
-
#   - - - def - webgl_vendor(page) -> None: -
- -
- View Source -
def webgl_vendor(page) -> None:
-    page.evaluateOnNewDocument(
-        """
-() => {
-    try {
-        const getParameter = WebGLRenderingContext.prototype.getParameter
-        WebGLRenderingContext.prototype.getParameter = function (parameter) {
-          if (parameter === 37445) {
-            return 'Intel Inc.'
-          }
-          if (parameter === 37446) {
-            return 'Intel Iris OpenGL Engine'
-          }
-          return getParameter.apply(this, [parameter])
-        }
-      } catch (err) {}
-}
-"""
-    )
-
- -
- - - -
-
-
#   - - - def - window_outerdimensions(page) -> None: -
- -
- View Source -
def window_outerdimensions(page) -> None:
-    page.evaluateOnNewDocument(
-        """
-() => {
-    try {
-        if (window.outerWidth && window.outerHeight) {
-            return
-        }
-        const windowFrame = 85
-        window.outerWidth = window.innerWidth
-        window.outerHeight = window.innerHeight + windowFrame
-    } catch (err) { }
-}
-"""
-    )
-
- -
- - - -
-
-
#   - - - def - stealth(page) -> None: -
- -
- View Source -
def stealth(page) -> None:
-    # chrome_runtime(page)
-    console_debug(page)
-    iframe_content_window(page)
-    # navigator_languages(page)
-    navigator_permissions(page)
-    navigator_plugins(page)
-    navigator_webdriver(page)
-    # navigator_vendor(page)
-    user_agent(page)
-    webgl_vendor(page)
-    window_outerdimensions(page)
-    media_codecs(page)
-
- -
- - - -
-
- - - - \ No newline at end of file diff --git a/docs/TikTokApi/exceptions.html b/docs/TikTokApi/exceptions.html deleted file mode 100644 index a3553d9b..00000000 --- a/docs/TikTokApi/exceptions.html +++ /dev/null @@ -1,664 +0,0 @@ - - - - - - - TikTokApi.exceptions API documentation - - - - - - - - -
-
-

-TikTokApi.exceptions

- - -
- View Source -
class TikTokCaptchaError(Exception):
-    def __init__(
-        self,
-        message="TikTok blocks this request displaying a Captcha \nTip: Consider using a proxy or a custom_verifyFp as method parameters",
-    ):
-        self.message = message
-        super().__init__(self.message)
-
-# 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 TikTokNotFoundError(Exception):
-    def __init__(self, message="The requested object does not exists"):
-        self.message = message
-        super().__init__(self.message)
-
-
-class EmptyResponseError(Exception):
-    def __init__(self, message="TikTok sent no data back"):
-        self.message = message
-        super().__init__(self.message)
-
-
-class JSONDecodeFailure(Exception):
-    def __init__(self, message="TikTok sent invalid JSON back"):
-        self.message = message
-        super().__init__(self.message)
-
-
-class TikTokNotAvailableError(Exception):
-    def __init__(self, message="The requested object is not available in this region"):
-        self.message = message
-        super().__init__(self.message)
-
- -
- -
-
-
- #   - - - class - TikTokCaptchaError(builtins.Exception): -
- -
- View Source -
class TikTokCaptchaError(Exception):
-    def __init__(
-        self,
-        message="TikTok blocks this request displaying a Captcha \nTip: Consider using a proxy or a custom_verifyFp as method parameters",
-    ):
-        self.message = message
-        super().__init__(self.message)
-
- -
- -

Common base class for all non-exit exceptions.

-
- - -
-
#   - - - TikTokCaptchaError( - message='TikTok blocks this request displaying a Captcha \nTip: Consider using a proxy or a custom_verifyFp as method parameters' -) -
- -
- View Source -
    def __init__(
-        self,
-        message="TikTok blocks this request displaying a Captcha \nTip: Consider using a proxy or a custom_verifyFp as method parameters",
-    ):
-        self.message = message
-        super().__init__(self.message)
-
- -
- - - -
-
-
Inherited Members
-
-
builtins.BaseException
-
with_traceback
-
args
- -
-
-
-
-
-
- #   - - - class - GenericTikTokError(builtins.Exception): -
- -
- View Source -
class GenericTikTokError(Exception):
-    def __init__(self, message):
-        self.message = message
-        super().__init__(self.message)
-
- -
- -

Common base class for all non-exit exceptions.

-
- - -
-
#   - - - GenericTikTokError(message) -
- -
- View Source -
    def __init__(self, message):
-        self.message = message
-        super().__init__(self.message)
-
- -
- - - -
-
-
Inherited Members
-
-
builtins.BaseException
-
with_traceback
-
args
- -
-
-
-
-
-
- #   - - - class - TikTokNotFoundError(builtins.Exception): -
- -
- View Source -
class TikTokNotFoundError(Exception):
-    def __init__(self, message="The requested object does not exists"):
-        self.message = message
-        super().__init__(self.message)
-
- -
- -

Common base class for all non-exit exceptions.

-
- - -
-
#   - - - TikTokNotFoundError(message='The requested object does not exists') -
- -
- View Source -
    def __init__(self, message="The requested object does not exists"):
-        self.message = message
-        super().__init__(self.message)
-
- -
- - - -
-
-
Inherited Members
-
-
builtins.BaseException
-
with_traceback
-
args
- -
-
-
-
-
-
- #   - - - class - EmptyResponseError(builtins.Exception): -
- -
- View Source -
class EmptyResponseError(Exception):
-    def __init__(self, message="TikTok sent no data back"):
-        self.message = message
-        super().__init__(self.message)
-
- -
- -

Common base class for all non-exit exceptions.

-
- - -
-
#   - - - EmptyResponseError(message='TikTok sent no data back') -
- -
- View Source -
    def __init__(self, message="TikTok sent no data back"):
-        self.message = message
-        super().__init__(self.message)
-
- -
- - - -
-
-
Inherited Members
-
-
builtins.BaseException
-
with_traceback
-
args
- -
-
-
-
-
-
- #   - - - class - JSONDecodeFailure(builtins.Exception): -
- -
- View Source -
class JSONDecodeFailure(Exception):
-    def __init__(self, message="TikTok sent invalid JSON back"):
-        self.message = message
-        super().__init__(self.message)
-
- -
- -

Common base class for all non-exit exceptions.

-
- - -
-
#   - - - JSONDecodeFailure(message='TikTok sent invalid JSON back') -
- -
- View Source -
    def __init__(self, message="TikTok sent invalid JSON back"):
-        self.message = message
-        super().__init__(self.message)
-
- -
- - - -
-
-
Inherited Members
-
-
builtins.BaseException
-
with_traceback
-
args
- -
-
-
-
-
-
- #   - - - class - TikTokNotAvailableError(builtins.Exception): -
- -
- View Source -
class TikTokNotAvailableError(Exception):
-    def __init__(self, message="The requested object is not available in this region"):
-        self.message = message
-        super().__init__(self.message)
-
- -
- -

Common base class for all non-exit exceptions.

-
- - -
-
#   - - - TikTokNotAvailableError(message='The requested object is not available in this region') -
- -
- View Source -
    def __init__(self, message="The requested object is not available in this region"):
-        self.message = message
-        super().__init__(self.message)
-
- -
- - - -
-
-
Inherited Members
-
-
builtins.BaseException
-
with_traceback
-
args
- -
-
-
-
-
- - - - \ No newline at end of file diff --git a/docs/TikTokApi/tiktok.html b/docs/TikTokApi/tiktok.html deleted file mode 100644 index a6f8e09f..00000000 --- a/docs/TikTokApi/tiktok.html +++ /dev/null @@ -1,5993 +0,0 @@ - - - - - - - TikTokApi.tiktok API documentation - - - - - - - - -
-
-

-TikTokApi.tiktok

- - -
- View Source -
import json
-import logging
-import os
-import random
-import string
-import time
-from urllib.parse import quote, urlencode
-
-import requests
-from playwright.sync_api import sync_playwright
-
-from .exceptions import *
-from .utilities import update_messager
-
-os.environ["no_proxy"] = "127.0.0.1,localhost"
-
-BASE_URL = "https://m.tiktok.com/"
-
-
-class TikTokApi:
-    __instance = None
-
-    def __init__(self, **kwargs):
-        """The TikTokApi class. Used to interact with TikTok, use get_instance NOT this."""
-        # Forces Singleton
-        if TikTokApi.__instance is None:
-            TikTokApi.__instance = self
-        else:
-            raise Exception("Only one TikTokApi object is allowed")
-        logging.basicConfig(level=kwargs.get("logging_level", logging.WARNING))
-        logging.info("Class initalized")
-
-        # Some Instance Vars
-        self.executablePath = kwargs.get("executablePath", None)
-
-        if kwargs.get("custom_did") != None:
-            raise Exception("Please use custom_device_id instead of custom_device_id")
-        self.custom_device_id = kwargs.get("custom_device_id", None)
-        self.userAgent = (
-            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
-            "AppleWebKit/537.36 (KHTML, like Gecko) "
-            "Chrome/86.0.4240.111 Safari/537.36"
-        )
-        self.proxy = kwargs.get("proxy", None)
-        self.custom_verifyFp = kwargs.get("custom_verifyFp")
-        self.signer_url = kwargs.get("external_signer", None)
-        self.request_delay = kwargs.get("request_delay", None)
-        self.requests_extra_kwargs = kwargs.get("requests_extra_kwargs", {})
-
-        if kwargs.get("use_test_endpoints", False):
-            global BASE_URL
-            BASE_URL = "https://t.tiktok.com/"
-        if kwargs.get("use_selenium", False):
-            from .browser_utilities.browser_selenium import browser
-        else:
-            from .browser_utilities.browser import browser
-
-        if kwargs.get("generate_static_device_id", False):
-            self.custom_device_id = "".join(
-                random.choice(string.digits) for num in range(19)
-            )
-
-        if self.signer_url is None:
-            self.browser = browser(**kwargs)
-            self.userAgent = self.browser.userAgent
-
-        try:
-            self.timezone_name = self.___format_new_params__(self.browser.timezone_name)
-            self.browser_language = self.___format_new_params__(
-                self.browser.browser_language
-            )
-            self.width = self.browser.width
-            self.height = self.browser.height
-            self.region = self.browser.region
-            self.language = self.browser.language
-        except Exception as e:
-            logging.exception(e)
-            logging.warning(
-                "An error ocurred while opening your browser but it was ignored."
-            )
-            logging.warning("Are you sure you ran python -m playwright install")
-
-            self.timezone_name = ""
-            self.browser_language = ""
-            self.width = "1920"
-            self.height = "1080"
-            self.region = "US"
-            self.language = "en"
-
-    @staticmethod
-    def get_instance(**kwargs):
-        """The TikTokApi class. Used to interact with TikTok. This is a singleton
-            class to prevent issues from arising with playwright
-
-        ##### Parameters
-        * logging_level: The logging level you want the program to run at, optional
-            These are the standard python logging module's levels.
-
-        * request_delay: The amount of time in seconds to wait before making a request, optional
-            This is used to throttle your own requests as you may end up making too
-            many requests to TikTok for your IP.
-
-        * custom_device_id: A TikTok parameter needed to download videos, optional
-            The code generates these and handles these pretty well itself, however
-            for some things such as video download you will need to set a consistent
-            one of these. All the methods take this as a optional parameter, however
-            it's cleaner code to store this at the instance level. You can override
-            this at the specific methods.
-
-        * generate_static_device_id: A parameter that generates a custom_device_id at the instance level
-            Use this if you want to download videos from a script but don't want to generate
-            your own custom_device_id parameter.
-
-        * custom_verifyFp: A TikTok parameter needed to work most of the time, optional
-            To get this parameter look at [this video](https://youtu.be/zwLmLfVI-VQ?t=117)
-            I recommend watching the entire thing, as it will help setup this package. All
-            the methods take this as a optional parameter, however it's cleaner code
-            to store this at the instance level. You can override this at the specific
-            methods.
-
-            You can use the following to generate `"".join(random.choice(string.digits)
-            for num in range(19))`
-
-        * use_test_endpoints: Send requests to TikTok's test endpoints, optional
-            This parameter when set to true will make requests to TikTok's testing
-            endpoints instead of the live site. I can't guarantee this will work
-            in the future, however currently basically any custom_verifyFp will
-            work here which is helpful.
-
-        * proxy: A string containing your proxy address, optional
-            If you want to do a lot of scraping of TikTok endpoints you'll likely
-            need a proxy.
-
-            Ex: "https://0.0.0.0:8080"
-
-            All the methods take this as a optional parameter, however it's cleaner code
-            to store this at the instance level. You can override this at the specific
-            methods.
-
-        * use_selenium: Option to use selenium over playwright, optional
-            Playwright is selected by default and is the one that I'm designing the
-            package to be compatable for, however if playwright doesn't work on
-            your machine feel free to set this to True.
-
-        * executablePath: The location of the driver, optional
-            This shouldn't be needed if you're using playwright
-
-        * **kwargs
-            Parameters that are passed on to basically every module and methods
-            that interact with this main class. These may or may not be documented
-            in other places.
-        """
-        if not TikTokApi.__instance:
-            TikTokApi(**kwargs)
-        return TikTokApi.__instance
-
-    def clean_up(self):
-        """A basic cleanup method, called automatically from the code"""
-        self.__del__()
-
-    def __del__(self):
-        """A basic cleanup method, called automatically from the code"""
-        try:
-            self.browser.clean_up()
-        except Exception:
-            pass
-        try:
-            get_playwright().stop()
-        except Exception:
-            pass
-        TikTokApi.__instance = None
-
-    def external_signer(self, url, custom_device_id=None, verifyFp=None):
-        """Makes requests to an external signer instead of using a browser.
-
-        ##### Parameters
-        * url: The server to make requests to
-            This server is designed to sign requests. You can find an example
-            of this signature server in the examples folder.
-
-        * custom_device_id: A TikTok parameter needed to download videos
-            The code generates these and handles these pretty well itself, however
-            for some things such as video download you will need to set a consistent
-            one of these.
-
-        * custom_verifyFp: A TikTok parameter needed to work most of the time,
-            To get this parameter look at [this video](https://youtu.be/zwLmLfVI-VQ?t=117)
-            I recommend watching the entire thing, as it will help setup this package.
-        """
-        if custom_device_id is not None:
-            query = {
-                "url": url,
-                "custom_device_id": custom_device_id,
-                "verifyFp": verifyFp,
-            }
-        else:
-            query = {"url": url, "verifyFp": verifyFp}
-        data = requests.get(
-            self.signer_url + "?{}".format(urlencode(query)),
-            **self.requests_extra_kwargs,
-        )
-        parsed_data = data.json()
-
-        return (
-            parsed_data["verifyFp"],
-            parsed_data["device_id"],
-            parsed_data["_signature"],
-            parsed_data["userAgent"],
-            parsed_data["referrer"],
-        )
-
-    def get_data(self, **kwargs) -> dict:
-        """Makes requests to TikTok and returns their JSON.
-
-        This is all handled by the package so it's unlikely
-        you will need to use this.
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        if self.request_delay is not None:
-            time.sleep(self.request_delay)
-
-        if self.proxy is not None:
-            proxy = self.proxy
-
-        if kwargs.get("custom_verifyFp") == None:
-            if self.custom_verifyFp != None:
-                verifyFp = self.custom_verifyFp
-            else:
-                verifyFp = "verify_khr3jabg_V7ucdslq_Vrw9_4KPb_AJ1b_Ks706M8zIJTq"
-        else:
-            verifyFp = kwargs.get("custom_verifyFp")
-
-        tt_params = None
-        send_tt_params = kwargs.get("send_tt_params", False)
-
-        if self.signer_url is None:
-            kwargs["custom_verifyFp"] = verifyFp
-            verify_fp, device_id, signature, tt_params = self.browser.sign_url(
-                calc_tt_params=send_tt_params, **kwargs
-            )
-            userAgent = self.browser.userAgent
-            referrer = self.browser.referrer
-        else:
-            verify_fp, device_id, signature, userAgent, referrer = self.external_signer(
-                kwargs["url"],
-                custom_device_id=kwargs.get("custom_device_id"),
-                verifyFp=kwargs.get("custom_verifyFp", verifyFp),
-            )
-
-        if not kwargs.get("send_tt_params", False):
-            tt_params = None
-
-        query = {"verifyFp": verify_fp, "device_id": device_id, "_signature": signature}
-        url = "{}&{}".format(kwargs["url"], urlencode(query))
-
-        h = requests.head(
-            url,
-            headers={"x-secsdk-csrf-version": "1.2.5", "x-secsdk-csrf-request": "1"},
-            proxies=self.__format_proxy(proxy),
-            **self.requests_extra_kwargs,
-        )
-        csrf_session_id = h.cookies["csrf_session_id"]
-        csrf_token = h.headers["X-Ware-Csrf-Token"].split(",")[1]
-        kwargs["csrf_session_id"] = csrf_session_id
-
-        headers = {
-            "authority": "m.tiktok.com",
-            "method": "GET",
-            "path": url.split("tiktok.com")[1],
-            "scheme": "https",
-            "accept": "application/json, text/plain, */*",
-            "accept-encoding": "gzip, deflate, br",
-            "accept-language": "en-US,en;q=0.9",
-            "origin": referrer,
-            "referer": referrer,
-            "sec-fetch-dest": "empty",
-            "sec-fetch-mode": "cors",
-            "sec-fetch-site": "same-site",
-            "sec-gpc": "1",
-            "user-agent": userAgent,
-            "x-secsdk-csrf-token": csrf_token,
-            "x-tt-params": tt_params,
-        }
-
-        logging.info(f"GET: {url}\n\theaders: {headers}")
-        r = requests.get(
-            url,
-            headers=headers,
-            cookies=self.get_cookies(**kwargs),
-            proxies=self.__format_proxy(proxy),
-            **self.requests_extra_kwargs,
-        )
-        try:
-            json = r.json()
-            if (
-                json.get("type") == "verify"
-                or json.get("verifyConfig", {}).get("type", "") == "verify"
-            ):
-                logging.error(
-                    "Tiktok wants to display a catcha. Response is:\n" + r.text
-                )
-                logging.error(self.get_cookies(**kwargs))
-                raise TikTokCaptchaError()
-            
-            # statusCode from props->pageProps->statusCode thanks @adiantek on #403
-            error_codes = {
-                "0": "OK",
-                "450": "CLIENT_PAGE_ERROR",
-                "10000": "VERIFY_CODE",
-                "10101": "SERVER_ERROR_NOT_500",
-                "10102": "USER_NOT_LOGIN",
-                "10111": "NET_ERROR",
-                "10113": "SHARK_SLIDE",
-                "10114": "SHARK_BLOCK",
-                "10119": "LIVE_NEED_LOGIN",
-                "10202": "USER_NOT_EXIST",
-                "10203": "MUSIC_NOT_EXIST",
-                "10204": "VIDEO_NOT_EXIST",
-                "10205": "HASHTAG_NOT_EXIST",
-                "10208": "EFFECT_NOT_EXIST",
-                "10209": "HASHTAG_BLACK_LIST",
-                "10210": "LIVE_NOT_EXIST",
-                "10211": "HASHTAG_SENSITIVITY_WORD",
-                "10212": "HASHTAG_UNSHELVE",
-                "10213": "VIDEO_LOW_AGE_M",
-                "10214": "VIDEO_LOW_AGE_T",
-                "10215": "VIDEO_ABNORMAL",
-                "10216": "VIDEO_PRIVATE_BY_USER",
-                "10217": "VIDEO_FIRST_REVIEW_UNSHELVE",
-                "10218": "MUSIC_UNSHELVE",
-                "10219": "MUSIC_NO_COPYRIGHT",
-                "10220": "VIDEO_UNSHELVE_BY_MUSIC",
-                "10221": "USER_BAN",
-                "10222": "USER_PRIVATE",
-                "10223": "USER_FTC",
-                "10224": "GAME_NOT_EXIST",
-                "10225": "USER_UNIQUE_SENSITIVITY",
-                "10227": "VIDEO_NEED_RECHECK",
-                "10228": "VIDEO_RISK",
-                "10229": "VIDEO_R_MASK",
-                "10230": "VIDEO_RISK_MASK",
-                "10231": "VIDEO_GEOFENCE_BLOCK",
-                "10404": "FYP_VIDEO_LIST_LIMIT",
-                "undefined": "MEDIA_ERROR"
-            }
-            statusCode = json.get("statusCode", 0)
-            logging.info(f"TikTok Returned: {json}")
-            if statusCode == 10201:
-                # Invalid Entity
-                raise TikTokNotFoundError(
-                    "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")
-            elif statusCode != 0 and statusCode != -1:
-                raise GenericTikTokError(error_codes.get(statusCode, f"TikTok sent an unknown StatusCode of {statusCode}"))
-
-            return r.json()
-        except ValueError as e:
-            text = r.text
-            logging.error("TikTok response: " + text)
-            if len(text) == 0:
-                raise EmptyResponseError(
-                    "Empty response from Tiktok to " + url
-                ) from None
-            else:
-                logging.error("Converting response to JSON failed")
-                logging.error(e)
-                raise JSONDecodeFailure() from e
-
-    def get_cookies(self, **kwargs):
-        """Extracts cookies from the kwargs passed to the function for get_data"""
-        device_id = kwargs.get(
-            "custom_device_id",
-            "".join(random.choice(string.digits) for num in range(19)),
-        )
-        if kwargs.get("custom_verifyFp") == None:
-            if self.custom_verifyFp != None:
-                verifyFp = self.custom_verifyFp
-            else:
-                verifyFp = "verify_khr3jabg_V7ucdslq_Vrw9_4KPb_AJ1b_Ks706M8zIJTq"
-        else:
-            verifyFp = kwargs.get("custom_verifyFp")
-
-        if kwargs.get("force_verify_fp_on_cookie_header", False):
-            return {
-                "tt_webid": device_id,
-                "tt_webid_v2": device_id,
-                "csrf_session_id": kwargs.get("csrf_session_id"),
-                "tt_csrf_token": "".join(
-                    random.choice(string.ascii_uppercase + string.ascii_lowercase)
-                    for i in range(16)
-                ),
-                "s_v_web_id": verifyFp,
-                "ttwid": kwargs.get("ttwid")
-            }
-        else:
-            return {
-                "tt_webid": device_id,
-                "tt_webid_v2": device_id,
-                "csrf_session_id": kwargs.get("csrf_session_id"),
-                "tt_csrf_token": "".join(
-                    random.choice(string.ascii_uppercase + string.ascii_lowercase)
-                    for i in range(16)
-                ),
-                "ttwid": kwargs.get("ttwid")
-            }
-
-    def get_bytes(self, **kwargs) -> bytes:
-        """Returns TikTok's response as bytes, similar to get_data"""
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        if self.signer_url is None:
-            verify_fp, device_id, signature, _ = self.browser.sign_url(
-                calc_tt_params=False, **kwargs
-            )
-            userAgent = self.browser.userAgent
-            referrer = self.browser.referrer
-        else:
-            verify_fp, device_id, signature, userAgent, referrer = self.external_signer(
-                kwargs["url"], custom_device_id=kwargs.get("custom_device_id", None)
-            )
-        query = {"verifyFp": verify_fp, "_signature": signature}
-        url = "{}&{}".format(kwargs["url"], urlencode(query))
-        r = requests.get(
-            url,
-            headers={
-                "Accept": "*/*",
-                "Accept-Encoding": "identity;q=1, *;q=0",
-                "Accept-Language": "en-US;en;q=0.9",
-                "Cache-Control": "no-cache",
-                "Connection": "keep-alive",
-                "Host": url.split("/")[2],
-                "Pragma": "no-cache",
-                "Range": "bytes=0-",
-                "Referer": "https://www.tiktok.com/",
-                "User-Agent": userAgent,
-            },
-            proxies=self.__format_proxy(proxy),
-            cookies=self.get_cookies(**kwargs),
-        )
-        return r.content
-
-    def by_trending(self, count=30, **kwargs) -> dict:
-        """
-        Gets trending TikToks
-
-        ##### Parameters
-        * count: The amount of TikToks you want returned, optional
-
-            Note: TikTok seems to only support at MOST ~2000 TikToks
-            from a single endpoint.
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        
-        spawn = requests.head(
-            "https://www.tiktok.com",
-            proxies=self.__format_proxy(proxy),
-            **self.requests_extra_kwargs
-        )
-        ttwid = spawn.cookies["ttwid"]
-        
-        response = []
-        first = True
-
-        while len(response) < count:
-            if count < maxCount:
-                realCount = count
-            else:
-                realCount = maxCount
-
-            query = {
-                "count": realCount,
-                "id": 1,
-                "sourceType": 12,
-                "itemID": 1,
-                "insertedItemID": "",
-                "region": region,
-                "priority_region": region,
-                "language": language,
-            }
-            api_url = "{}api/recommend/item_list/?{}&{}".format(
-                BASE_URL, self.__add_url_params__(), urlencode(query)
-            )
-            res = self.get_data(url=api_url, ttwid=ttwid, **kwargs)
-            for t in res.get("itemList", []):
-                response.append(t)
-
-            if not res.get("hasMore", False) and not first:
-                logging.info("TikTok isn't sending more TikToks beyond this point.")
-                return response[:count]
-
-            realCount = count - len(response)
-
-            first = False
-
-        return response[:count]
-
-    def search_for_users(self, search_term, count=28, **kwargs) -> list:
-        """Returns a list of users that match the search_term
-
-        ##### Parameters
-        * search_term: The string to search for users by
-            This string is the term you want to search for users by.
-
-        * count: The number of users to return
-            Note: maximum is around 28 for this type of endpoint.
-        """
-        return self.discover_type(search_term, prefix="user", count=count, **kwargs)
-
-    def search_for_music(self, search_term, count=28, **kwargs) -> list:
-        """Returns a list of music that match the search_term
-
-        ##### Parameters
-        * search_term: The string to search for music by
-            This string is the term you want to search for music by.
-
-        * count: The number of music to return
-            Note: maximum is around 28 for this type of endpoint.
-        """
-        return self.discover_type(search_term, prefix="music", count=count, **kwargs)
-
-    def search_for_hashtags(self, search_term, count=28, **kwargs) -> list:
-        """Returns a list of hashtags that match the search_term
-
-        ##### Parameters
-        * search_term: The string to search for music by
-            This string is the term you want to search for music by.
-
-        * count: The number of music to return
-            Note: maximum is around 28 for this type of endpoint.
-        """
-        return self.discover_type(
-            search_term, prefix="challenge", count=count, **kwargs
-        )
-
-    def discover_type(self, search_term, prefix, count=28, offset=0, **kwargs) -> list:
-        """Returns a list of whatever the prefix type you pass in
-
-        ##### Parameters
-        * search_term: The string to search by
-
-        * prefix: The prefix of what to search for
-
-        * count: The number search results to return
-            Note: maximum is around 28 for this type of endpoint.
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        response = []
-        while len(response) < count:
-            query = {
-                "discoverType": 0,
-                "needItemList": False,
-                "keyWord": search_term,
-                "offset": offset,
-                "count": count,
-                "useRecommend": False,
-                "language": "en",
-            }
-            api_url = "{}api/discover/{}/?{}&{}".format(
-                BASE_URL, prefix, self.__add_url_params__(), urlencode(query)
-            )
-            data = self.get_data(url=api_url, **kwargs)
-
-            if "userInfoList" in data.keys():
-                for x in data["userInfoList"]:
-                    response.append(x)
-            elif "musicInfoList" in data.keys():
-                for x in data["musicInfoList"]:
-                    response.append(x)
-            elif "challengeInfoList" in data.keys():
-                for x in data["challengeInfoList"]:
-                    response.append(x)
-            else:
-                logging.info("TikTok is not sending videos beyond this point.")
-                break
-
-            offset += maxCount
-
-        return response[:count]
-
-    def user_posts(self, userID, secUID, count=30, cursor=0, **kwargs) -> dict:
-        """Returns an array of dictionaries representing TikToks for a user.
-
-        ##### Parameters
-        * userID: The userID of the user, which TikTok assigns
-
-            You can find this from utilizing other methods or
-            just use by_username to find it.
-        * secUID: The secUID of the user, which TikTok assigns
-
-            You can find this from utilizing other methods or
-            just use by_username to find it.
-        * count: The number of posts to return
-
-            Note: seems to only support up to ~2,000
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        response = []
-        first = True
-
-        while len(response) < count:
-            if count < maxCount:
-                realCount = count
-            else:
-                realCount = maxCount
-
-            query = {
-                "count": realCount,
-                "id": userID,
-                "cursor": cursor,
-                "type": 1,
-                "secUid": secUID,
-                "sourceType": 8,
-                "appId": 1233,
-                "region": region,
-                "priority_region": region,
-                "language": language,
-            }
-            api_url = "{}api/post/item_list/?{}&{}".format(
-                BASE_URL, self.__add_url_params__(), urlencode(query)
-            )
-
-            res = self.get_data(url=api_url, send_tt_params=True, **kwargs)
-
-            if "itemList" in res.keys():
-                for t in res.get("itemList", []):
-                    response.append(t)
-
-            if not res.get("hasMore", False) and not first:
-                logging.info("TikTok isn't sending more TikToks beyond this point.")
-                return response
-
-            realCount = count - len(response)
-            cursor = res["cursor"]
-
-            first = False
-
-        return response[:count]
-
-    def by_username(self, username, count=30, **kwargs) -> dict:
-        """Returns a dictionary listing TikToks given a user's username.
-
-        ##### Parameters
-        * username: The username of the TikTok user to get TikToks from
-
-        * count: The number of posts to return
-
-            Note: seems to only support up to ~2,000
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        data = self.get_user_object(username, **kwargs)
-        return self.user_posts(
-            data["id"],
-            data["secUid"],
-            count=count,
-            **kwargs,
-        )
-
-    def user_page(self, userID, secUID, page_size=30, cursor=0, **kwargs) -> dict:
-        """Returns a dictionary listing of one page of TikToks given a user's ID and secUID
-
-        ##### Parameters
-        * userID: The userID of the user, which TikTok assigns
-
-            You can find this from utilizing other methods or
-            just use by_username to find it.
-        * secUID: The secUID of the user, which TikTok assigns
-
-            You can find this from utilizing other methods or
-            just use by_username to find it.
-        * page_size: The number of posts to return per page
-
-            Gets a specific page of a user, doesn't iterate.
-        * cursor: The offset of a page
-
-            The offset to return new videos from
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        api_url = (
-            BASE_URL + "api/post/item_list/?{}&count={}&id={}&type=1&secUid={}"
-            "&cursor={}&sourceType=8&appId=1233&region={}&language={}".format(
-                self.__add_url_params__(),
-                page_size,
-                str(userID),
-                str(secUID),
-                cursor,
-                region,
-                language,
-            )
-        )
-
-        return self.get_data(url=api_url, send_tt_params=True, **kwargs)
-
-    def get_user_pager(self, username, page_size=30, cursor=0, **kwargs):
-        """Returns a generator to page through a user's feed
-
-        ##### Parameters
-        * username: The username of the user
-
-        * page_size: The number of posts to return in a page
-
-        * cursor: The offset of a page
-
-            The offset to return new videos from
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        data = self.get_user_object(username, **kwargs)
-
-        while True:
-            resp = self.user_page(
-                data["id"],
-                data["secUid"],
-                page_size=page_size,
-                cursor=cursor,
-                **kwargs,
-            )
-
-            try:
-                page = resp["itemList"]
-            except KeyError:
-                # No mo results
-                return
-
-            cursor = resp["cursor"]
-
-            yield page
-
-            if not resp["hasMore"]:
-                return  # all done
-
-    def user_liked(self, userID, secUID, count=30, cursor=0, **kwargs) -> dict:
-        """Returns a dictionary listing TikToks that a given a user has liked.
-            Note: The user's likes must be public
-
-        ##### Parameters
-        * userID: The userID of the user, which TikTok assigns
-
-        * secUID: The secUID of the user, which TikTok assigns
-
-        * count: The number of posts to return
-
-            Note: seems to only support up to ~2,000
-        * cursor: The offset of a page
-
-            The offset to return new videos from
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        response = []
-        first = True
-
-        while len(response) < count:
-            if count < maxCount:
-                realCount = count
-            else:
-                realCount = maxCount
-
-            query = {
-                "count": realCount,
-                "id": userID,
-                "type": 2,
-                "secUid": secUID,
-                "cursor": cursor,
-                "sourceType": 9,
-                "appId": 1233,
-                "region": region,
-                "priority_region": region,
-                "language": language,
-            }
-            api_url = "{}api/favorite/item_list/?{}&{}".format(
-                BASE_URL, self.__add_url_params__(), urlencode(query)
-            )
-
-            res = self.get_data(url=api_url, **kwargs)
-
-            try:
-                res["itemList"]
-            except Exception:
-                logging.error("User's likes are most likely private")
-                return []
-
-            for t in res.get("itemList", []):
-                response.append(t)
-
-            if not res.get("hasMore", False) and not first:
-                logging.info("TikTok isn't sending more TikToks beyond this point.")
-                return response
-
-            realCount = count - len(response)
-            cursor = res["cursor"]
-
-            first = False
-
-        return response[:count]
-
-    def user_liked_by_username(self, username, count=30, **kwargs) -> dict:
-        """Returns a dictionary listing TikToks a user has liked by username.
-            Note: The user's likes must be public
-
-        ##### Parameters
-        * username: The username of the user
-
-        * count: The number of posts to return
-
-            Note: seems to only support up to ~2,000
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        data = self.get_user_object(username, **kwargs)
-        return self.user_liked(
-            data["id"],
-            data["secUid"],
-            count=count,
-            **kwargs,
-        )
-
-    def by_sound(self, id, count=30, offset=0, **kwargs) -> dict:
-        """Returns a dictionary listing TikToks with a specific sound.
-
-        ##### Parameters
-        * id: The sound id to search by
-
-            Note: Can be found in the URL of the sound specific page or with other methods.
-        * count: The number of posts to return
-
-            Note: seems to only support up to ~2,000
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        response = []
-
-        while len(response) < count:
-            if count < maxCount:
-                realCount = count
-            else:
-                realCount = maxCount
-
-            query = {
-                "secUid": "",
-                "musicID": str(id),
-                "count": str(realCount),
-                "cursor": offset,
-                "shareUid": "",
-                "language": language,
-            }
-            api_url = "{}api/music/item_list/?{}&{}".format(
-                BASE_URL, self.__add_url_params__(), urlencode(query)
-            )
-
-            res = self.get_data(url=api_url, send_tt_params=True, **kwargs)
-
-            try:
-                for t in res["items"]:
-                    response.append(t)
-            except KeyError:
-                for t in res.get("itemList", []):
-                    response.append(t)
-
-            if not res.get("hasMore", False):
-                logging.info("TikTok isn't sending more TikToks beyond this point.")
-                return response
-
-            realCount = count - len(response)
-            offset = res["cursor"]
-
-        return response[:count]
-
-    def by_sound_page(self, id, page_size=30, cursor=0, **kwargs) -> dict:
-        """Returns a page of tiktoks with a specific sound.
-
-        Parameters
-        ----------
-        id: The sound id to search by
-            Note: Can be found in the URL of the sound specific page or with other methods.
-        cursor: offset for pagination
-        page_size: The number of posts to return
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        query = {
-            "musicID": str(id),
-            "count": str(page_size),
-            "cursor": cursor,
-            "language": language,
-        }
-        api_url = "{}api/music/item_list/?{}&{}".format(
-            BASE_URL, self.__add_url_params__(), urlencode(query)
-        )
-
-        return self.get_data(url=api_url, send_tt_params=True, **kwargs)
-
-    def get_music_object(self, id, **kwargs) -> dict:
-        """Returns a music object for a specific sound id.
-
-        ##### Parameters
-        * id: The sound id to get the object for
-
-            This can be found by using other methods.
-        """
-        return self.get_music_object_full(id, **kwargs)["music"]
-
-    def get_music_object_full(self, id, **kwargs):
-        """Returns a music object for a specific sound id.
-
-        ##### Parameters
-        * id: The sound id to get the object for
-
-            This can be found by using other methods.
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        r = requests.get(
-            "https://www.tiktok.com/music/-{}".format(id),
-            headers={
-                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
-                "Accept-Encoding": "gzip, deflate",
-                "Connection": "keep-alive",
-                "User-Agent": self.userAgent,
-            },
-            proxies=self.__format_proxy(kwargs.get("proxy", None)),
-            cookies=self.get_cookies(**kwargs),
-            **self.requests_extra_kwargs,
-        )
-
-        j_raw = self.__extract_tag_contents(r.text)
-        return json.loads(j_raw)["props"]["pageProps"]["musicInfo"]
-
-    def get_music_object_full_by_api(self, id, **kwargs):
-        """Returns a music object for a specific sound id, but using the API rather than HTML requests.
-
-        ##### Parameters
-        * id: The sound id to get the object for
-
-            This can be found by using other methods.
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        api_url = "{}node/share/music/-{}?{}".format(
-            BASE_URL, id, self.__add_url_params__()
-        )
-        res = self.get_data(url=api_url, **kwargs)
-
-        if res.get("statusCode", 200) == 10203:
-            raise TikTokNotFoundError()
-
-        return res["musicInfo"]
-
-    def by_hashtag(self, hashtag, count=30, offset=0, **kwargs) -> dict:
-        """Returns a dictionary listing TikToks with a specific hashtag.
-
-        ##### Parameters
-        * hashtag: The hashtag to search by
-
-            Without the # symbol
-
-            A valid string is "funny"
-        * count: The number of posts to return
-            Note: seems to only support up to ~2,000
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        id = self.get_hashtag_object(hashtag)["challengeInfo"]["challenge"]["id"]
-        response = []
-
-        required_count = count
-
-        while len(response) < required_count:
-            if count > maxCount:
-                count = maxCount
-            query = {
-                "count": count,
-                "challengeID": id,
-                "type": 3,
-                "secUid": "",
-                "cursor": offset,
-                "priority_region": "",
-            }
-            api_url = "{}api/challenge/item_list/?{}&{}".format(
-                BASE_URL, self.__add_url_params__(), urlencode(query)
-            )
-            res = self.get_data(url=api_url, **kwargs)
-
-            for t in res.get("itemList", []):
-                response.append(t)
-
-            if not res.get("hasMore", False):
-                logging.info("TikTok isn't sending more TikToks beyond this point.")
-                return response
-
-            offset += maxCount
-
-        return response[:required_count]
-
-    def get_hashtag_object(self, hashtag, **kwargs) -> dict:
-        """Returns a hashtag object.
-
-        ##### Parameters
-        * hashtag: The hashtag to search by
-
-            Without the # symbol
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        query = {"name": hashtag, "isName": True, "lang": language}
-        api_url = "{}node/share/tag/{}?{}&{}".format(
-            BASE_URL, quote(hashtag), self.__add_url_params__(), urlencode(query)
-        )
-        data = self.get_data(url=api_url, **kwargs)
-        if data["challengeInfo"].get("challenge") is None:
-            raise TikTokNotFoundError("Challenge {} does not exist".format(hashtag))
-        return data
-
-    def get_recommended_tiktoks_by_video_id(self, id, count=30, **kwargs) -> dict:
-        """Returns a dictionary listing reccomended TikToks for a specific TikTok video.
-
-
-        ##### Parameters
-        * id: The id of the video to get suggestions for
-
-            Can be found using other methods
-        * count: The count of results you want to return
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        response = []
-        first = True
-
-        while len(response) < count:
-            if count < maxCount:
-                realCount = count
-            else:
-                realCount = maxCount
-
-            query = {
-                "count": realCount,
-                "id": 1,
-                "secUid": "",
-                "sourceType": 12,
-                "appId": 1233,
-                "region": region,
-                "priority_region": region,
-                "language": language,
-            }
-            api_url = "{}api/recommend/item_list/?{}&{}".format(
-                BASE_URL, self.__add_url_params__(), urlencode(query)
-            )
-
-            res = self.get_data(url=api_url, **kwargs)
-
-            for t in res.get("itemList", []):
-                response.append(t)
-
-            if not res.get("hasMore", False) and not first:
-                logging.info("TikTok isn't sending more TikToks beyond this point.")
-                return response[:count]
-
-            realCount = count - len(response)
-
-            first = False
-
-        return response[:count]
-
-    def get_tiktok_by_id(self, id, **kwargs) -> dict:
-        """Returns a dictionary of a specific TikTok.
-
-        ##### Parameters
-        * id: The id of the TikTok you want to get the object for
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        device_id = kwargs.get("custom_device_id", None)
-        query = {
-            "itemId": id,
-            "language": language,
-        }
-        api_url = "{}api/item/detail/?{}&{}".format(
-            BASE_URL, self.__add_url_params__(), urlencode(query)
-        )
-
-        return self.get_data(url=api_url, **kwargs)
-
-    def get_tiktok_by_url(self, url, **kwargs) -> dict:
-        """Returns a dictionary of a TikTok object by url.
-
-
-        ##### Parameters
-        * url: The TikTok url you want to retrieve
-
-        """
-
-        url = requests.head(url=url, allow_redirects=True).url
-
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        custom_device_id = kwargs.get("custom_device_id", None)
-        if "@" in url and "/video/" in url:
-            post_id = url.split("/video/")[1].split("?")[0]
-        else:
-            raise Exception(
-                "URL format not supported. Below is an example of a supported url.\n"
-                "https://www.tiktok.com/@therock/video/6829267836783971589"
-            )
-
-        return self.get_tiktok_by_id(
-            post_id,
-            **kwargs,
-        )
-
-    def get_tiktok_by_html(self, url, **kwargs) -> dict:
-        """This method retrieves a TikTok using the html
-        endpoints rather than the API based ones.
-
-        ##### Parameters
-        * url: The url of the TikTok to get
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-
-        r = requests.get(
-            url,
-            headers={
-                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
-                "path": url.split("tiktok.com")[1],
-                "Accept-Encoding": "gzip, deflate",
-                "Connection": "keep-alive",
-                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
-            },
-            proxies=self.__format_proxy(kwargs.get("proxy", None)),
-            cookies=self.get_cookies(**kwargs),
-            **self.requests_extra_kwargs,
-        )
-
-        t = r.text
-        try:
-            j_raw = self.__extract_tag_contents(r.text)
-        except IndexError:
-            if not t:
-                logging.error("TikTok response is empty")
-            else:
-                logging.error("TikTok response: \n " + t)
-            raise TikTokCaptchaError()
-
-        data = json.loads(j_raw)["props"]["pageProps"]
-
-        if data["serverCode"] == 404:
-            raise TikTokNotFoundError("TikTok with that url doesn't exist")
-
-        return data
-
-    def get_user_object(self, username, **kwargs) -> dict:
-        """Gets a user object (dictionary)
-
-        ##### Parameters
-        * username: The username of the user
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        return self.get_user(username, **kwargs)["userInfo"]["user"]
-
-    def get_user(self, username, **kwargs) -> dict:
-        """Gets the full exposed user object
-
-        ##### Parameters
-        * username: The username of the user
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        r = requests.get(
-            "https://tiktok.com/@{}?lang=en".format(quote(username)),
-            headers={
-                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
-                "path": "/@{}".format(quote(username)),
-                "Accept-Encoding": "gzip, deflate",
-                "Connection": "keep-alive",
-                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
-            },
-            proxies=self.__format_proxy(kwargs.get("proxy", None)),
-            cookies=self.get_cookies(**kwargs),
-            **self.requests_extra_kwargs,
-        )
-
-        t = r.text
-
-        try:
-            j_raw = self.__extract_tag_contents(r.text)
-        except IndexError:
-            if not t:
-                logging.error("Tiktok response is empty")
-            else:
-                logging.error("Tiktok response: \n " + t)
-            raise TikTokCaptchaError()
-
-        user = json.loads(j_raw)["props"]["pageProps"]
-
-        if user["serverCode"] == 404:
-            raise TikTokNotFoundError(
-                "TikTok user with username {} does not exist".format(username)
-            )
-
-        return user
-
-    def get_video_by_tiktok(self, data, **kwargs) -> bytes:
-        """Downloads video from TikTok using a TikTok object.
-
-        You will need to set a custom_device_id to do this for anything but trending.
-        To do this, this is pretty simple you can either generate one yourself or,
-        you can pass the generate_static_device_id=True into the constructor of the
-        TikTokApi class.
-
-        ##### Parameters
-        * data: A TikTok object
-
-            A TikTok JSON object from any other method.
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        try:
-            api_url = data["video"]["downloadAddr"]
-        except Exception:
-            try:
-                api_url = data["itemInfos"]["video"]["urls"][0]
-            except Exception:
-                api_url = data["itemInfo"]["itemStruct"]["video"]["playAddr"]
-        return self.get_video_by_download_url(api_url, **kwargs)
-
-    def get_video_by_download_url(self, download_url, **kwargs) -> bytes:
-        """Downloads video from TikTok using download url in a TikTok object
-
-        ##### Parameters
-        * download_url: The download url key value in a TikTok object
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        return self.get_bytes(url=download_url, **kwargs)
-
-    def get_video_by_url(self, video_url, **kwargs) -> bytes:
-        """Downloads a TikTok video by a URL
-
-        ##### Parameters
-        * video_url: The TikTok url to download the video from
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        tiktok_schema = self.get_tiktok_by_url(video_url, **kwargs)
-        download_url = tiktok_schema["itemInfo"]["itemStruct"]["video"]["downloadAddr"]
-
-        return self.get_bytes(url=download_url, **kwargs)
-
-    def get_video_no_watermark(self, video_url, return_bytes=1, **kwargs) -> bytes:
-        """Gets the video with no watermark
-        .. deprecated::
-
-        Deprecated due to TikTok fixing this
-
-        ##### Parameters
-        * video_url: The url of the video you want to download
-
-        * return_bytes: Set this to 0 if you want url, 1 if you want bytes
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        raise Exception("Deprecated method, TikTok fixed this.")
-
-    def get_music_title(self, id, **kwargs):
-        """Retrieves a music title given an ID
-
-        ##### Parameters
-        * id: The music id to get the title for
-        """
-        r = requests.get(
-            "https://www.tiktok.com/music/-{}".format(id),
-            headers={
-                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
-                "Accept-Encoding": "gzip, deflate",
-                "Connection": "keep-alive",
-                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
-            },
-            proxies=self.__format_proxy(kwargs.get("proxy", None)),
-            cookies=self.get_cookies(**kwargs),
-            **self.requests_extra_kwargs,
-        )
-        t = r.text
-        j_raw = self.__extract_tag_contents(r.text)
-
-        music_object = json.loads(j_raw)["props"]["pageProps"]["musicInfo"]
-        if not music_object.get("title", None):
-            raise TikTokNotFoundError("Song of {} id does not exist".format(str(id)))
-
-        return music_object["title"]
-
-    def get_secuid(self, username, **kwargs):
-        """Gets the secUid for a specific username
-
-        ##### Parameters
-        * username: The username to get the secUid for
-        """
-        r = requests.get(
-            "https://tiktok.com/@{}?lang=en".format(quote(username)),
-            headers={
-                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
-                "path": "/@{}".format(quote(username)),
-                "Accept-Encoding": "gzip, deflate",
-                "Connection": "keep-alive",
-                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
-            },
-            proxies=self.__format_proxy(
-                kwargs.get("proxy", None), cookies=self.get_cookies(**kwargs)
-            ),
-            **self.requests_extra_kwargs,
-        )
-        try:
-            return r.text.split('"secUid":"')[1].split('","secret":')[0]
-        except IndexError as e:
-            logging.info(r.text)
-            logging.error(e)
-            raise Exception(
-                "Retrieving the user secUid failed. Likely due to TikTok wanting captcha validation. Try to use a proxy."
-            )
-
-    @staticmethod
-    def generate_device_id():
-        """Generates a valid device_id for other methods. Pass this as the custom_device_id field to download videos"""
-        return "".join([random.choice(string.digits) for num in range(19)])
-
-    #
-    # PRIVATE METHODS
-    #
-
-    def __format_proxy(self, proxy) -> dict:
-        """
-        Formats the proxy object
-        """
-        if proxy is None and self.proxy is not None:
-            proxy = self.proxy
-        if proxy is not None:
-            return {"http": proxy, "https": proxy}
-        else:
-            return None
-
-    def __get_js(self, proxy=None) -> str:
-        return requests.get(
-            "https://sf16-muse-va.ibytedtos.com/obj/rc-web-sdk-gcs/acrawler.js",
-            proxies=self.__format_proxy(proxy),
-            **self.requests_extra_kwargs,
-        ).text
-
-    def ___format_new_params__(self, parm) -> str:
-        # TODO: Maybe try not doing this? It should be handled by the urlencode.
-        return parm.replace("/", "%2F").replace(" ", "+").replace(";", "%3B")
-
-    def __add_url_params__(self) -> str:
-        query = {
-            "aid": 1988,
-            "app_name": "tiktok_web",
-            "device_platform": "web_mobile",
-            "region": self.region or "US",
-            "priority_region": "",
-            "os": "ios",
-            "referer": "",
-            "root_referer": "",
-            "cookie_enabled": "true",
-            "screen_width": self.width,
-            "screen_height": self.height,
-            "browser_language": self.browser_language.lower() or "en-us",
-            "browser_platform": "iPhone",
-            "browser_name": "Mozilla",
-            "browser_version": self.___format_new_params__(self.userAgent),
-            "browser_online": "true",
-            "timezone_name": self.timezone_name or "America/Chicago",
-            "is_page_visible": "true",
-            "focus_state": "true",
-            "is_fullscreen": "false",
-            "history_len": random.randint(0, 30),
-            "language": self.language or "en",
-        }
-        return urlencode(query)
-
-    def __extract_tag_contents(self, html):
-        nonce_start = '<head nonce="'
-        nonce_end = '">'
-        nonce = html.split(nonce_start)[1].split(nonce_end)[0]
-        j_raw = html.split(
-            '<script id="__NEXT_DATA__" type="application/json" nonce="%s" crossorigin="anonymous">'
-            % nonce
-        )[1].split("</script>")[0]
-        return j_raw
-
-    # Process the kwargs
-    def __process_kwargs__(self, kwargs):
-        region = kwargs.get("region", "US")
-        language = kwargs.get("language", "en")
-        proxy = kwargs.get("proxy", None)
-        maxCount = kwargs.get("maxCount", 35)
-
-        if kwargs.get("custom_device_id", None) != None:
-            device_id = kwargs.get("custom_device_id")
-        else:
-            if self.custom_device_id != None:
-                device_id = self.custom_device_id
-            else:
-                device_id = "".join(random.choice(string.digits) for num in range(19))
-        return region, language, proxy, maxCount, device_id
-
- -
- -
-
-
- #   - - - class - TikTokApi: -
- -
- View Source -
class TikTokApi:
-    __instance = None
-
-    def __init__(self, **kwargs):
-        """The TikTokApi class. Used to interact with TikTok, use get_instance NOT this."""
-        # Forces Singleton
-        if TikTokApi.__instance is None:
-            TikTokApi.__instance = self
-        else:
-            raise Exception("Only one TikTokApi object is allowed")
-        logging.basicConfig(level=kwargs.get("logging_level", logging.WARNING))
-        logging.info("Class initalized")
-
-        # Some Instance Vars
-        self.executablePath = kwargs.get("executablePath", None)
-
-        if kwargs.get("custom_did") != None:
-            raise Exception("Please use custom_device_id instead of custom_device_id")
-        self.custom_device_id = kwargs.get("custom_device_id", None)
-        self.userAgent = (
-            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
-            "AppleWebKit/537.36 (KHTML, like Gecko) "
-            "Chrome/86.0.4240.111 Safari/537.36"
-        )
-        self.proxy = kwargs.get("proxy", None)
-        self.custom_verifyFp = kwargs.get("custom_verifyFp")
-        self.signer_url = kwargs.get("external_signer", None)
-        self.request_delay = kwargs.get("request_delay", None)
-        self.requests_extra_kwargs = kwargs.get("requests_extra_kwargs", {})
-
-        if kwargs.get("use_test_endpoints", False):
-            global BASE_URL
-            BASE_URL = "https://t.tiktok.com/"
-        if kwargs.get("use_selenium", False):
-            from .browser_utilities.browser_selenium import browser
-        else:
-            from .browser_utilities.browser import browser
-
-        if kwargs.get("generate_static_device_id", False):
-            self.custom_device_id = "".join(
-                random.choice(string.digits) for num in range(19)
-            )
-
-        if self.signer_url is None:
-            self.browser = browser(**kwargs)
-            self.userAgent = self.browser.userAgent
-
-        try:
-            self.timezone_name = self.___format_new_params__(self.browser.timezone_name)
-            self.browser_language = self.___format_new_params__(
-                self.browser.browser_language
-            )
-            self.width = self.browser.width
-            self.height = self.browser.height
-            self.region = self.browser.region
-            self.language = self.browser.language
-        except Exception as e:
-            logging.exception(e)
-            logging.warning(
-                "An error ocurred while opening your browser but it was ignored."
-            )
-            logging.warning("Are you sure you ran python -m playwright install")
-
-            self.timezone_name = ""
-            self.browser_language = ""
-            self.width = "1920"
-            self.height = "1080"
-            self.region = "US"
-            self.language = "en"
-
-    @staticmethod
-    def get_instance(**kwargs):
-        """The TikTokApi class. Used to interact with TikTok. This is a singleton
-            class to prevent issues from arising with playwright
-
-        ##### Parameters
-        * logging_level: The logging level you want the program to run at, optional
-            These are the standard python logging module's levels.
-
-        * request_delay: The amount of time in seconds to wait before making a request, optional
-            This is used to throttle your own requests as you may end up making too
-            many requests to TikTok for your IP.
-
-        * custom_device_id: A TikTok parameter needed to download videos, optional
-            The code generates these and handles these pretty well itself, however
-            for some things such as video download you will need to set a consistent
-            one of these. All the methods take this as a optional parameter, however
-            it's cleaner code to store this at the instance level. You can override
-            this at the specific methods.
-
-        * generate_static_device_id: A parameter that generates a custom_device_id at the instance level
-            Use this if you want to download videos from a script but don't want to generate
-            your own custom_device_id parameter.
-
-        * custom_verifyFp: A TikTok parameter needed to work most of the time, optional
-            To get this parameter look at [this video](https://youtu.be/zwLmLfVI-VQ?t=117)
-            I recommend watching the entire thing, as it will help setup this package. All
-            the methods take this as a optional parameter, however it's cleaner code
-            to store this at the instance level. You can override this at the specific
-            methods.
-
-            You can use the following to generate `"".join(random.choice(string.digits)
-            for num in range(19))`
-
-        * use_test_endpoints: Send requests to TikTok's test endpoints, optional
-            This parameter when set to true will make requests to TikTok's testing
-            endpoints instead of the live site. I can't guarantee this will work
-            in the future, however currently basically any custom_verifyFp will
-            work here which is helpful.
-
-        * proxy: A string containing your proxy address, optional
-            If you want to do a lot of scraping of TikTok endpoints you'll likely
-            need a proxy.
-
-            Ex: "https://0.0.0.0:8080"
-
-            All the methods take this as a optional parameter, however it's cleaner code
-            to store this at the instance level. You can override this at the specific
-            methods.
-
-        * use_selenium: Option to use selenium over playwright, optional
-            Playwright is selected by default and is the one that I'm designing the
-            package to be compatable for, however if playwright doesn't work on
-            your machine feel free to set this to True.
-
-        * executablePath: The location of the driver, optional
-            This shouldn't be needed if you're using playwright
-
-        * **kwargs
-            Parameters that are passed on to basically every module and methods
-            that interact with this main class. These may or may not be documented
-            in other places.
-        """
-        if not TikTokApi.__instance:
-            TikTokApi(**kwargs)
-        return TikTokApi.__instance
-
-    def clean_up(self):
-        """A basic cleanup method, called automatically from the code"""
-        self.__del__()
-
-    def __del__(self):
-        """A basic cleanup method, called automatically from the code"""
-        try:
-            self.browser.clean_up()
-        except Exception:
-            pass
-        try:
-            get_playwright().stop()
-        except Exception:
-            pass
-        TikTokApi.__instance = None
-
-    def external_signer(self, url, custom_device_id=None, verifyFp=None):
-        """Makes requests to an external signer instead of using a browser.
-
-        ##### Parameters
-        * url: The server to make requests to
-            This server is designed to sign requests. You can find an example
-            of this signature server in the examples folder.
-
-        * custom_device_id: A TikTok parameter needed to download videos
-            The code generates these and handles these pretty well itself, however
-            for some things such as video download you will need to set a consistent
-            one of these.
-
-        * custom_verifyFp: A TikTok parameter needed to work most of the time,
-            To get this parameter look at [this video](https://youtu.be/zwLmLfVI-VQ?t=117)
-            I recommend watching the entire thing, as it will help setup this package.
-        """
-        if custom_device_id is not None:
-            query = {
-                "url": url,
-                "custom_device_id": custom_device_id,
-                "verifyFp": verifyFp,
-            }
-        else:
-            query = {"url": url, "verifyFp": verifyFp}
-        data = requests.get(
-            self.signer_url + "?{}".format(urlencode(query)),
-            **self.requests_extra_kwargs,
-        )
-        parsed_data = data.json()
-
-        return (
-            parsed_data["verifyFp"],
-            parsed_data["device_id"],
-            parsed_data["_signature"],
-            parsed_data["userAgent"],
-            parsed_data["referrer"],
-        )
-
-    def get_data(self, **kwargs) -> dict:
-        """Makes requests to TikTok and returns their JSON.
-
-        This is all handled by the package so it's unlikely
-        you will need to use this.
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        if self.request_delay is not None:
-            time.sleep(self.request_delay)
-
-        if self.proxy is not None:
-            proxy = self.proxy
-
-        if kwargs.get("custom_verifyFp") == None:
-            if self.custom_verifyFp != None:
-                verifyFp = self.custom_verifyFp
-            else:
-                verifyFp = "verify_khr3jabg_V7ucdslq_Vrw9_4KPb_AJ1b_Ks706M8zIJTq"
-        else:
-            verifyFp = kwargs.get("custom_verifyFp")
-
-        tt_params = None
-        send_tt_params = kwargs.get("send_tt_params", False)
-
-        if self.signer_url is None:
-            kwargs["custom_verifyFp"] = verifyFp
-            verify_fp, device_id, signature, tt_params = self.browser.sign_url(
-                calc_tt_params=send_tt_params, **kwargs
-            )
-            userAgent = self.browser.userAgent
-            referrer = self.browser.referrer
-        else:
-            verify_fp, device_id, signature, userAgent, referrer = self.external_signer(
-                kwargs["url"],
-                custom_device_id=kwargs.get("custom_device_id"),
-                verifyFp=kwargs.get("custom_verifyFp", verifyFp),
-            )
-
-        if not kwargs.get("send_tt_params", False):
-            tt_params = None
-
-        query = {"verifyFp": verify_fp, "device_id": device_id, "_signature": signature}
-        url = "{}&{}".format(kwargs["url"], urlencode(query))
-
-        h = requests.head(
-            url,
-            headers={"x-secsdk-csrf-version": "1.2.5", "x-secsdk-csrf-request": "1"},
-            proxies=self.__format_proxy(proxy),
-            **self.requests_extra_kwargs,
-        )
-        csrf_session_id = h.cookies["csrf_session_id"]
-        csrf_token = h.headers["X-Ware-Csrf-Token"].split(",")[1]
-        kwargs["csrf_session_id"] = csrf_session_id
-
-        headers = {
-            "authority": "m.tiktok.com",
-            "method": "GET",
-            "path": url.split("tiktok.com")[1],
-            "scheme": "https",
-            "accept": "application/json, text/plain, */*",
-            "accept-encoding": "gzip, deflate, br",
-            "accept-language": "en-US,en;q=0.9",
-            "origin": referrer,
-            "referer": referrer,
-            "sec-fetch-dest": "empty",
-            "sec-fetch-mode": "cors",
-            "sec-fetch-site": "same-site",
-            "sec-gpc": "1",
-            "user-agent": userAgent,
-            "x-secsdk-csrf-token": csrf_token,
-            "x-tt-params": tt_params,
-        }
-
-        logging.info(f"GET: {url}\n\theaders: {headers}")
-        r = requests.get(
-            url,
-            headers=headers,
-            cookies=self.get_cookies(**kwargs),
-            proxies=self.__format_proxy(proxy),
-            **self.requests_extra_kwargs,
-        )
-        try:
-            json = r.json()
-            if (
-                json.get("type") == "verify"
-                or json.get("verifyConfig", {}).get("type", "") == "verify"
-            ):
-                logging.error(
-                    "Tiktok wants to display a catcha. Response is:\n" + r.text
-                )
-                logging.error(self.get_cookies(**kwargs))
-                raise TikTokCaptchaError()
-            
-            # statusCode from props->pageProps->statusCode thanks @adiantek on #403
-            error_codes = {
-                "0": "OK",
-                "450": "CLIENT_PAGE_ERROR",
-                "10000": "VERIFY_CODE",
-                "10101": "SERVER_ERROR_NOT_500",
-                "10102": "USER_NOT_LOGIN",
-                "10111": "NET_ERROR",
-                "10113": "SHARK_SLIDE",
-                "10114": "SHARK_BLOCK",
-                "10119": "LIVE_NEED_LOGIN",
-                "10202": "USER_NOT_EXIST",
-                "10203": "MUSIC_NOT_EXIST",
-                "10204": "VIDEO_NOT_EXIST",
-                "10205": "HASHTAG_NOT_EXIST",
-                "10208": "EFFECT_NOT_EXIST",
-                "10209": "HASHTAG_BLACK_LIST",
-                "10210": "LIVE_NOT_EXIST",
-                "10211": "HASHTAG_SENSITIVITY_WORD",
-                "10212": "HASHTAG_UNSHELVE",
-                "10213": "VIDEO_LOW_AGE_M",
-                "10214": "VIDEO_LOW_AGE_T",
-                "10215": "VIDEO_ABNORMAL",
-                "10216": "VIDEO_PRIVATE_BY_USER",
-                "10217": "VIDEO_FIRST_REVIEW_UNSHELVE",
-                "10218": "MUSIC_UNSHELVE",
-                "10219": "MUSIC_NO_COPYRIGHT",
-                "10220": "VIDEO_UNSHELVE_BY_MUSIC",
-                "10221": "USER_BAN",
-                "10222": "USER_PRIVATE",
-                "10223": "USER_FTC",
-                "10224": "GAME_NOT_EXIST",
-                "10225": "USER_UNIQUE_SENSITIVITY",
-                "10227": "VIDEO_NEED_RECHECK",
-                "10228": "VIDEO_RISK",
-                "10229": "VIDEO_R_MASK",
-                "10230": "VIDEO_RISK_MASK",
-                "10231": "VIDEO_GEOFENCE_BLOCK",
-                "10404": "FYP_VIDEO_LIST_LIMIT",
-                "undefined": "MEDIA_ERROR"
-            }
-            statusCode = json.get("statusCode", 0)
-            logging.info(f"TikTok Returned: {json}")
-            if statusCode == 10201:
-                # Invalid Entity
-                raise TikTokNotFoundError(
-                    "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")
-            elif statusCode != 0 and statusCode != -1:
-                raise GenericTikTokError(error_codes.get(statusCode, f"TikTok sent an unknown StatusCode of {statusCode}"))
-
-            return r.json()
-        except ValueError as e:
-            text = r.text
-            logging.error("TikTok response: " + text)
-            if len(text) == 0:
-                raise EmptyResponseError(
-                    "Empty response from Tiktok to " + url
-                ) from None
-            else:
-                logging.error("Converting response to JSON failed")
-                logging.error(e)
-                raise JSONDecodeFailure() from e
-
-    def get_cookies(self, **kwargs):
-        """Extracts cookies from the kwargs passed to the function for get_data"""
-        device_id = kwargs.get(
-            "custom_device_id",
-            "".join(random.choice(string.digits) for num in range(19)),
-        )
-        if kwargs.get("custom_verifyFp") == None:
-            if self.custom_verifyFp != None:
-                verifyFp = self.custom_verifyFp
-            else:
-                verifyFp = "verify_khr3jabg_V7ucdslq_Vrw9_4KPb_AJ1b_Ks706M8zIJTq"
-        else:
-            verifyFp = kwargs.get("custom_verifyFp")
-
-        if kwargs.get("force_verify_fp_on_cookie_header", False):
-            return {
-                "tt_webid": device_id,
-                "tt_webid_v2": device_id,
-                "csrf_session_id": kwargs.get("csrf_session_id"),
-                "tt_csrf_token": "".join(
-                    random.choice(string.ascii_uppercase + string.ascii_lowercase)
-                    for i in range(16)
-                ),
-                "s_v_web_id": verifyFp,
-                "ttwid": kwargs.get("ttwid")
-            }
-        else:
-            return {
-                "tt_webid": device_id,
-                "tt_webid_v2": device_id,
-                "csrf_session_id": kwargs.get("csrf_session_id"),
-                "tt_csrf_token": "".join(
-                    random.choice(string.ascii_uppercase + string.ascii_lowercase)
-                    for i in range(16)
-                ),
-                "ttwid": kwargs.get("ttwid")
-            }
-
-    def get_bytes(self, **kwargs) -> bytes:
-        """Returns TikTok's response as bytes, similar to get_data"""
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        if self.signer_url is None:
-            verify_fp, device_id, signature, _ = self.browser.sign_url(
-                calc_tt_params=False, **kwargs
-            )
-            userAgent = self.browser.userAgent
-            referrer = self.browser.referrer
-        else:
-            verify_fp, device_id, signature, userAgent, referrer = self.external_signer(
-                kwargs["url"], custom_device_id=kwargs.get("custom_device_id", None)
-            )
-        query = {"verifyFp": verify_fp, "_signature": signature}
-        url = "{}&{}".format(kwargs["url"], urlencode(query))
-        r = requests.get(
-            url,
-            headers={
-                "Accept": "*/*",
-                "Accept-Encoding": "identity;q=1, *;q=0",
-                "Accept-Language": "en-US;en;q=0.9",
-                "Cache-Control": "no-cache",
-                "Connection": "keep-alive",
-                "Host": url.split("/")[2],
-                "Pragma": "no-cache",
-                "Range": "bytes=0-",
-                "Referer": "https://www.tiktok.com/",
-                "User-Agent": userAgent,
-            },
-            proxies=self.__format_proxy(proxy),
-            cookies=self.get_cookies(**kwargs),
-        )
-        return r.content
-
-    def by_trending(self, count=30, **kwargs) -> dict:
-        """
-        Gets trending TikToks
-
-        ##### Parameters
-        * count: The amount of TikToks you want returned, optional
-
-            Note: TikTok seems to only support at MOST ~2000 TikToks
-            from a single endpoint.
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        
-        spawn = requests.head(
-            "https://www.tiktok.com",
-            proxies=self.__format_proxy(proxy),
-            **self.requests_extra_kwargs
-        )
-        ttwid = spawn.cookies["ttwid"]
-        
-        response = []
-        first = True
-
-        while len(response) < count:
-            if count < maxCount:
-                realCount = count
-            else:
-                realCount = maxCount
-
-            query = {
-                "count": realCount,
-                "id": 1,
-                "sourceType": 12,
-                "itemID": 1,
-                "insertedItemID": "",
-                "region": region,
-                "priority_region": region,
-                "language": language,
-            }
-            api_url = "{}api/recommend/item_list/?{}&{}".format(
-                BASE_URL, self.__add_url_params__(), urlencode(query)
-            )
-            res = self.get_data(url=api_url, ttwid=ttwid, **kwargs)
-            for t in res.get("itemList", []):
-                response.append(t)
-
-            if not res.get("hasMore", False) and not first:
-                logging.info("TikTok isn't sending more TikToks beyond this point.")
-                return response[:count]
-
-            realCount = count - len(response)
-
-            first = False
-
-        return response[:count]
-
-    def search_for_users(self, search_term, count=28, **kwargs) -> list:
-        """Returns a list of users that match the search_term
-
-        ##### Parameters
-        * search_term: The string to search for users by
-            This string is the term you want to search for users by.
-
-        * count: The number of users to return
-            Note: maximum is around 28 for this type of endpoint.
-        """
-        return self.discover_type(search_term, prefix="user", count=count, **kwargs)
-
-    def search_for_music(self, search_term, count=28, **kwargs) -> list:
-        """Returns a list of music that match the search_term
-
-        ##### Parameters
-        * search_term: The string to search for music by
-            This string is the term you want to search for music by.
-
-        * count: The number of music to return
-            Note: maximum is around 28 for this type of endpoint.
-        """
-        return self.discover_type(search_term, prefix="music", count=count, **kwargs)
-
-    def search_for_hashtags(self, search_term, count=28, **kwargs) -> list:
-        """Returns a list of hashtags that match the search_term
-
-        ##### Parameters
-        * search_term: The string to search for music by
-            This string is the term you want to search for music by.
-
-        * count: The number of music to return
-            Note: maximum is around 28 for this type of endpoint.
-        """
-        return self.discover_type(
-            search_term, prefix="challenge", count=count, **kwargs
-        )
-
-    def discover_type(self, search_term, prefix, count=28, offset=0, **kwargs) -> list:
-        """Returns a list of whatever the prefix type you pass in
-
-        ##### Parameters
-        * search_term: The string to search by
-
-        * prefix: The prefix of what to search for
-
-        * count: The number search results to return
-            Note: maximum is around 28 for this type of endpoint.
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        response = []
-        while len(response) < count:
-            query = {
-                "discoverType": 0,
-                "needItemList": False,
-                "keyWord": search_term,
-                "offset": offset,
-                "count": count,
-                "useRecommend": False,
-                "language": "en",
-            }
-            api_url = "{}api/discover/{}/?{}&{}".format(
-                BASE_URL, prefix, self.__add_url_params__(), urlencode(query)
-            )
-            data = self.get_data(url=api_url, **kwargs)
-
-            if "userInfoList" in data.keys():
-                for x in data["userInfoList"]:
-                    response.append(x)
-            elif "musicInfoList" in data.keys():
-                for x in data["musicInfoList"]:
-                    response.append(x)
-            elif "challengeInfoList" in data.keys():
-                for x in data["challengeInfoList"]:
-                    response.append(x)
-            else:
-                logging.info("TikTok is not sending videos beyond this point.")
-                break
-
-            offset += maxCount
-
-        return response[:count]
-
-    def user_posts(self, userID, secUID, count=30, cursor=0, **kwargs) -> dict:
-        """Returns an array of dictionaries representing TikToks for a user.
-
-        ##### Parameters
-        * userID: The userID of the user, which TikTok assigns
-
-            You can find this from utilizing other methods or
-            just use by_username to find it.
-        * secUID: The secUID of the user, which TikTok assigns
-
-            You can find this from utilizing other methods or
-            just use by_username to find it.
-        * count: The number of posts to return
-
-            Note: seems to only support up to ~2,000
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        response = []
-        first = True
-
-        while len(response) < count:
-            if count < maxCount:
-                realCount = count
-            else:
-                realCount = maxCount
-
-            query = {
-                "count": realCount,
-                "id": userID,
-                "cursor": cursor,
-                "type": 1,
-                "secUid": secUID,
-                "sourceType": 8,
-                "appId": 1233,
-                "region": region,
-                "priority_region": region,
-                "language": language,
-            }
-            api_url = "{}api/post/item_list/?{}&{}".format(
-                BASE_URL, self.__add_url_params__(), urlencode(query)
-            )
-
-            res = self.get_data(url=api_url, send_tt_params=True, **kwargs)
-
-            if "itemList" in res.keys():
-                for t in res.get("itemList", []):
-                    response.append(t)
-
-            if not res.get("hasMore", False) and not first:
-                logging.info("TikTok isn't sending more TikToks beyond this point.")
-                return response
-
-            realCount = count - len(response)
-            cursor = res["cursor"]
-
-            first = False
-
-        return response[:count]
-
-    def by_username(self, username, count=30, **kwargs) -> dict:
-        """Returns a dictionary listing TikToks given a user's username.
-
-        ##### Parameters
-        * username: The username of the TikTok user to get TikToks from
-
-        * count: The number of posts to return
-
-            Note: seems to only support up to ~2,000
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        data = self.get_user_object(username, **kwargs)
-        return self.user_posts(
-            data["id"],
-            data["secUid"],
-            count=count,
-            **kwargs,
-        )
-
-    def user_page(self, userID, secUID, page_size=30, cursor=0, **kwargs) -> dict:
-        """Returns a dictionary listing of one page of TikToks given a user's ID and secUID
-
-        ##### Parameters
-        * userID: The userID of the user, which TikTok assigns
-
-            You can find this from utilizing other methods or
-            just use by_username to find it.
-        * secUID: The secUID of the user, which TikTok assigns
-
-            You can find this from utilizing other methods or
-            just use by_username to find it.
-        * page_size: The number of posts to return per page
-
-            Gets a specific page of a user, doesn't iterate.
-        * cursor: The offset of a page
-
-            The offset to return new videos from
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        api_url = (
-            BASE_URL + "api/post/item_list/?{}&count={}&id={}&type=1&secUid={}"
-            "&cursor={}&sourceType=8&appId=1233&region={}&language={}".format(
-                self.__add_url_params__(),
-                page_size,
-                str(userID),
-                str(secUID),
-                cursor,
-                region,
-                language,
-            )
-        )
-
-        return self.get_data(url=api_url, send_tt_params=True, **kwargs)
-
-    def get_user_pager(self, username, page_size=30, cursor=0, **kwargs):
-        """Returns a generator to page through a user's feed
-
-        ##### Parameters
-        * username: The username of the user
-
-        * page_size: The number of posts to return in a page
-
-        * cursor: The offset of a page
-
-            The offset to return new videos from
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        data = self.get_user_object(username, **kwargs)
-
-        while True:
-            resp = self.user_page(
-                data["id"],
-                data["secUid"],
-                page_size=page_size,
-                cursor=cursor,
-                **kwargs,
-            )
-
-            try:
-                page = resp["itemList"]
-            except KeyError:
-                # No mo results
-                return
-
-            cursor = resp["cursor"]
-
-            yield page
-
-            if not resp["hasMore"]:
-                return  # all done
-
-    def user_liked(self, userID, secUID, count=30, cursor=0, **kwargs) -> dict:
-        """Returns a dictionary listing TikToks that a given a user has liked.
-            Note: The user's likes must be public
-
-        ##### Parameters
-        * userID: The userID of the user, which TikTok assigns
-
-        * secUID: The secUID of the user, which TikTok assigns
-
-        * count: The number of posts to return
-
-            Note: seems to only support up to ~2,000
-        * cursor: The offset of a page
-
-            The offset to return new videos from
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        response = []
-        first = True
-
-        while len(response) < count:
-            if count < maxCount:
-                realCount = count
-            else:
-                realCount = maxCount
-
-            query = {
-                "count": realCount,
-                "id": userID,
-                "type": 2,
-                "secUid": secUID,
-                "cursor": cursor,
-                "sourceType": 9,
-                "appId": 1233,
-                "region": region,
-                "priority_region": region,
-                "language": language,
-            }
-            api_url = "{}api/favorite/item_list/?{}&{}".format(
-                BASE_URL, self.__add_url_params__(), urlencode(query)
-            )
-
-            res = self.get_data(url=api_url, **kwargs)
-
-            try:
-                res["itemList"]
-            except Exception:
-                logging.error("User's likes are most likely private")
-                return []
-
-            for t in res.get("itemList", []):
-                response.append(t)
-
-            if not res.get("hasMore", False) and not first:
-                logging.info("TikTok isn't sending more TikToks beyond this point.")
-                return response
-
-            realCount = count - len(response)
-            cursor = res["cursor"]
-
-            first = False
-
-        return response[:count]
-
-    def user_liked_by_username(self, username, count=30, **kwargs) -> dict:
-        """Returns a dictionary listing TikToks a user has liked by username.
-            Note: The user's likes must be public
-
-        ##### Parameters
-        * username: The username of the user
-
-        * count: The number of posts to return
-
-            Note: seems to only support up to ~2,000
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        data = self.get_user_object(username, **kwargs)
-        return self.user_liked(
-            data["id"],
-            data["secUid"],
-            count=count,
-            **kwargs,
-        )
-
-    def by_sound(self, id, count=30, offset=0, **kwargs) -> dict:
-        """Returns a dictionary listing TikToks with a specific sound.
-
-        ##### Parameters
-        * id: The sound id to search by
-
-            Note: Can be found in the URL of the sound specific page or with other methods.
-        * count: The number of posts to return
-
-            Note: seems to only support up to ~2,000
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        response = []
-
-        while len(response) < count:
-            if count < maxCount:
-                realCount = count
-            else:
-                realCount = maxCount
-
-            query = {
-                "secUid": "",
-                "musicID": str(id),
-                "count": str(realCount),
-                "cursor": offset,
-                "shareUid": "",
-                "language": language,
-            }
-            api_url = "{}api/music/item_list/?{}&{}".format(
-                BASE_URL, self.__add_url_params__(), urlencode(query)
-            )
-
-            res = self.get_data(url=api_url, send_tt_params=True, **kwargs)
-
-            try:
-                for t in res["items"]:
-                    response.append(t)
-            except KeyError:
-                for t in res.get("itemList", []):
-                    response.append(t)
-
-            if not res.get("hasMore", False):
-                logging.info("TikTok isn't sending more TikToks beyond this point.")
-                return response
-
-            realCount = count - len(response)
-            offset = res["cursor"]
-
-        return response[:count]
-
-    def by_sound_page(self, id, page_size=30, cursor=0, **kwargs) -> dict:
-        """Returns a page of tiktoks with a specific sound.
-
-        Parameters
-        ----------
-        id: The sound id to search by
-            Note: Can be found in the URL of the sound specific page or with other methods.
-        cursor: offset for pagination
-        page_size: The number of posts to return
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        query = {
-            "musicID": str(id),
-            "count": str(page_size),
-            "cursor": cursor,
-            "language": language,
-        }
-        api_url = "{}api/music/item_list/?{}&{}".format(
-            BASE_URL, self.__add_url_params__(), urlencode(query)
-        )
-
-        return self.get_data(url=api_url, send_tt_params=True, **kwargs)
-
-    def get_music_object(self, id, **kwargs) -> dict:
-        """Returns a music object for a specific sound id.
-
-        ##### Parameters
-        * id: The sound id to get the object for
-
-            This can be found by using other methods.
-        """
-        return self.get_music_object_full(id, **kwargs)["music"]
-
-    def get_music_object_full(self, id, **kwargs):
-        """Returns a music object for a specific sound id.
-
-        ##### Parameters
-        * id: The sound id to get the object for
-
-            This can be found by using other methods.
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        r = requests.get(
-            "https://www.tiktok.com/music/-{}".format(id),
-            headers={
-                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
-                "Accept-Encoding": "gzip, deflate",
-                "Connection": "keep-alive",
-                "User-Agent": self.userAgent,
-            },
-            proxies=self.__format_proxy(kwargs.get("proxy", None)),
-            cookies=self.get_cookies(**kwargs),
-            **self.requests_extra_kwargs,
-        )
-
-        j_raw = self.__extract_tag_contents(r.text)
-        return json.loads(j_raw)["props"]["pageProps"]["musicInfo"]
-
-    def get_music_object_full_by_api(self, id, **kwargs):
-        """Returns a music object for a specific sound id, but using the API rather than HTML requests.
-
-        ##### Parameters
-        * id: The sound id to get the object for
-
-            This can be found by using other methods.
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        api_url = "{}node/share/music/-{}?{}".format(
-            BASE_URL, id, self.__add_url_params__()
-        )
-        res = self.get_data(url=api_url, **kwargs)
-
-        if res.get("statusCode", 200) == 10203:
-            raise TikTokNotFoundError()
-
-        return res["musicInfo"]
-
-    def by_hashtag(self, hashtag, count=30, offset=0, **kwargs) -> dict:
-        """Returns a dictionary listing TikToks with a specific hashtag.
-
-        ##### Parameters
-        * hashtag: The hashtag to search by
-
-            Without the # symbol
-
-            A valid string is "funny"
-        * count: The number of posts to return
-            Note: seems to only support up to ~2,000
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        id = self.get_hashtag_object(hashtag)["challengeInfo"]["challenge"]["id"]
-        response = []
-
-        required_count = count
-
-        while len(response) < required_count:
-            if count > maxCount:
-                count = maxCount
-            query = {
-                "count": count,
-                "challengeID": id,
-                "type": 3,
-                "secUid": "",
-                "cursor": offset,
-                "priority_region": "",
-            }
-            api_url = "{}api/challenge/item_list/?{}&{}".format(
-                BASE_URL, self.__add_url_params__(), urlencode(query)
-            )
-            res = self.get_data(url=api_url, **kwargs)
-
-            for t in res.get("itemList", []):
-                response.append(t)
-
-            if not res.get("hasMore", False):
-                logging.info("TikTok isn't sending more TikToks beyond this point.")
-                return response
-
-            offset += maxCount
-
-        return response[:required_count]
-
-    def get_hashtag_object(self, hashtag, **kwargs) -> dict:
-        """Returns a hashtag object.
-
-        ##### Parameters
-        * hashtag: The hashtag to search by
-
-            Without the # symbol
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        query = {"name": hashtag, "isName": True, "lang": language}
-        api_url = "{}node/share/tag/{}?{}&{}".format(
-            BASE_URL, quote(hashtag), self.__add_url_params__(), urlencode(query)
-        )
-        data = self.get_data(url=api_url, **kwargs)
-        if data["challengeInfo"].get("challenge") is None:
-            raise TikTokNotFoundError("Challenge {} does not exist".format(hashtag))
-        return data
-
-    def get_recommended_tiktoks_by_video_id(self, id, count=30, **kwargs) -> dict:
-        """Returns a dictionary listing reccomended TikToks for a specific TikTok video.
-
-
-        ##### Parameters
-        * id: The id of the video to get suggestions for
-
-            Can be found using other methods
-        * count: The count of results you want to return
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        response = []
-        first = True
-
-        while len(response) < count:
-            if count < maxCount:
-                realCount = count
-            else:
-                realCount = maxCount
-
-            query = {
-                "count": realCount,
-                "id": 1,
-                "secUid": "",
-                "sourceType": 12,
-                "appId": 1233,
-                "region": region,
-                "priority_region": region,
-                "language": language,
-            }
-            api_url = "{}api/recommend/item_list/?{}&{}".format(
-                BASE_URL, self.__add_url_params__(), urlencode(query)
-            )
-
-            res = self.get_data(url=api_url, **kwargs)
-
-            for t in res.get("itemList", []):
-                response.append(t)
-
-            if not res.get("hasMore", False) and not first:
-                logging.info("TikTok isn't sending more TikToks beyond this point.")
-                return response[:count]
-
-            realCount = count - len(response)
-
-            first = False
-
-        return response[:count]
-
-    def get_tiktok_by_id(self, id, **kwargs) -> dict:
-        """Returns a dictionary of a specific TikTok.
-
-        ##### Parameters
-        * id: The id of the TikTok you want to get the object for
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        device_id = kwargs.get("custom_device_id", None)
-        query = {
-            "itemId": id,
-            "language": language,
-        }
-        api_url = "{}api/item/detail/?{}&{}".format(
-            BASE_URL, self.__add_url_params__(), urlencode(query)
-        )
-
-        return self.get_data(url=api_url, **kwargs)
-
-    def get_tiktok_by_url(self, url, **kwargs) -> dict:
-        """Returns a dictionary of a TikTok object by url.
-
-
-        ##### Parameters
-        * url: The TikTok url you want to retrieve
-
-        """
-
-        url = requests.head(url=url, allow_redirects=True).url
-
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        custom_device_id = kwargs.get("custom_device_id", None)
-        if "@" in url and "/video/" in url:
-            post_id = url.split("/video/")[1].split("?")[0]
-        else:
-            raise Exception(
-                "URL format not supported. Below is an example of a supported url.\n"
-                "https://www.tiktok.com/@therock/video/6829267836783971589"
-            )
-
-        return self.get_tiktok_by_id(
-            post_id,
-            **kwargs,
-        )
-
-    def get_tiktok_by_html(self, url, **kwargs) -> dict:
-        """This method retrieves a TikTok using the html
-        endpoints rather than the API based ones.
-
-        ##### Parameters
-        * url: The url of the TikTok to get
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-
-        r = requests.get(
-            url,
-            headers={
-                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
-                "path": url.split("tiktok.com")[1],
-                "Accept-Encoding": "gzip, deflate",
-                "Connection": "keep-alive",
-                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
-            },
-            proxies=self.__format_proxy(kwargs.get("proxy", None)),
-            cookies=self.get_cookies(**kwargs),
-            **self.requests_extra_kwargs,
-        )
-
-        t = r.text
-        try:
-            j_raw = self.__extract_tag_contents(r.text)
-        except IndexError:
-            if not t:
-                logging.error("TikTok response is empty")
-            else:
-                logging.error("TikTok response: \n " + t)
-            raise TikTokCaptchaError()
-
-        data = json.loads(j_raw)["props"]["pageProps"]
-
-        if data["serverCode"] == 404:
-            raise TikTokNotFoundError("TikTok with that url doesn't exist")
-
-        return data
-
-    def get_user_object(self, username, **kwargs) -> dict:
-        """Gets a user object (dictionary)
-
-        ##### Parameters
-        * username: The username of the user
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        return self.get_user(username, **kwargs)["userInfo"]["user"]
-
-    def get_user(self, username, **kwargs) -> dict:
-        """Gets the full exposed user object
-
-        ##### Parameters
-        * username: The username of the user
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        r = requests.get(
-            "https://tiktok.com/@{}?lang=en".format(quote(username)),
-            headers={
-                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
-                "path": "/@{}".format(quote(username)),
-                "Accept-Encoding": "gzip, deflate",
-                "Connection": "keep-alive",
-                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
-            },
-            proxies=self.__format_proxy(kwargs.get("proxy", None)),
-            cookies=self.get_cookies(**kwargs),
-            **self.requests_extra_kwargs,
-        )
-
-        t = r.text
-
-        try:
-            j_raw = self.__extract_tag_contents(r.text)
-        except IndexError:
-            if not t:
-                logging.error("Tiktok response is empty")
-            else:
-                logging.error("Tiktok response: \n " + t)
-            raise TikTokCaptchaError()
-
-        user = json.loads(j_raw)["props"]["pageProps"]
-
-        if user["serverCode"] == 404:
-            raise TikTokNotFoundError(
-                "TikTok user with username {} does not exist".format(username)
-            )
-
-        return user
-
-    def get_video_by_tiktok(self, data, **kwargs) -> bytes:
-        """Downloads video from TikTok using a TikTok object.
-
-        You will need to set a custom_device_id to do this for anything but trending.
-        To do this, this is pretty simple you can either generate one yourself or,
-        you can pass the generate_static_device_id=True into the constructor of the
-        TikTokApi class.
-
-        ##### Parameters
-        * data: A TikTok object
-
-            A TikTok JSON object from any other method.
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        try:
-            api_url = data["video"]["downloadAddr"]
-        except Exception:
-            try:
-                api_url = data["itemInfos"]["video"]["urls"][0]
-            except Exception:
-                api_url = data["itemInfo"]["itemStruct"]["video"]["playAddr"]
-        return self.get_video_by_download_url(api_url, **kwargs)
-
-    def get_video_by_download_url(self, download_url, **kwargs) -> bytes:
-        """Downloads video from TikTok using download url in a TikTok object
-
-        ##### Parameters
-        * download_url: The download url key value in a TikTok object
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        return self.get_bytes(url=download_url, **kwargs)
-
-    def get_video_by_url(self, video_url, **kwargs) -> bytes:
-        """Downloads a TikTok video by a URL
-
-        ##### Parameters
-        * video_url: The TikTok url to download the video from
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        tiktok_schema = self.get_tiktok_by_url(video_url, **kwargs)
-        download_url = tiktok_schema["itemInfo"]["itemStruct"]["video"]["downloadAddr"]
-
-        return self.get_bytes(url=download_url, **kwargs)
-
-    def get_video_no_watermark(self, video_url, return_bytes=1, **kwargs) -> bytes:
-        """Gets the video with no watermark
-        .. deprecated::
-
-        Deprecated due to TikTok fixing this
-
-        ##### Parameters
-        * video_url: The url of the video you want to download
-
-        * return_bytes: Set this to 0 if you want url, 1 if you want bytes
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        raise Exception("Deprecated method, TikTok fixed this.")
-
-    def get_music_title(self, id, **kwargs):
-        """Retrieves a music title given an ID
-
-        ##### Parameters
-        * id: The music id to get the title for
-        """
-        r = requests.get(
-            "https://www.tiktok.com/music/-{}".format(id),
-            headers={
-                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
-                "Accept-Encoding": "gzip, deflate",
-                "Connection": "keep-alive",
-                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
-            },
-            proxies=self.__format_proxy(kwargs.get("proxy", None)),
-            cookies=self.get_cookies(**kwargs),
-            **self.requests_extra_kwargs,
-        )
-        t = r.text
-        j_raw = self.__extract_tag_contents(r.text)
-
-        music_object = json.loads(j_raw)["props"]["pageProps"]["musicInfo"]
-        if not music_object.get("title", None):
-            raise TikTokNotFoundError("Song of {} id does not exist".format(str(id)))
-
-        return music_object["title"]
-
-    def get_secuid(self, username, **kwargs):
-        """Gets the secUid for a specific username
-
-        ##### Parameters
-        * username: The username to get the secUid for
-        """
-        r = requests.get(
-            "https://tiktok.com/@{}?lang=en".format(quote(username)),
-            headers={
-                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
-                "path": "/@{}".format(quote(username)),
-                "Accept-Encoding": "gzip, deflate",
-                "Connection": "keep-alive",
-                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
-            },
-            proxies=self.__format_proxy(
-                kwargs.get("proxy", None), cookies=self.get_cookies(**kwargs)
-            ),
-            **self.requests_extra_kwargs,
-        )
-        try:
-            return r.text.split('"secUid":"')[1].split('","secret":')[0]
-        except IndexError as e:
-            logging.info(r.text)
-            logging.error(e)
-            raise Exception(
-                "Retrieving the user secUid failed. Likely due to TikTok wanting captcha validation. Try to use a proxy."
-            )
-
-    @staticmethod
-    def generate_device_id():
-        """Generates a valid device_id for other methods. Pass this as the custom_device_id field to download videos"""
-        return "".join([random.choice(string.digits) for num in range(19)])
-
-    #
-    # PRIVATE METHODS
-    #
-
-    def __format_proxy(self, proxy) -> dict:
-        """
-        Formats the proxy object
-        """
-        if proxy is None and self.proxy is not None:
-            proxy = self.proxy
-        if proxy is not None:
-            return {"http": proxy, "https": proxy}
-        else:
-            return None
-
-    def __get_js(self, proxy=None) -> str:
-        return requests.get(
-            "https://sf16-muse-va.ibytedtos.com/obj/rc-web-sdk-gcs/acrawler.js",
-            proxies=self.__format_proxy(proxy),
-            **self.requests_extra_kwargs,
-        ).text
-
-    def ___format_new_params__(self, parm) -> str:
-        # TODO: Maybe try not doing this? It should be handled by the urlencode.
-        return parm.replace("/", "%2F").replace(" ", "+").replace(";", "%3B")
-
-    def __add_url_params__(self) -> str:
-        query = {
-            "aid": 1988,
-            "app_name": "tiktok_web",
-            "device_platform": "web_mobile",
-            "region": self.region or "US",
-            "priority_region": "",
-            "os": "ios",
-            "referer": "",
-            "root_referer": "",
-            "cookie_enabled": "true",
-            "screen_width": self.width,
-            "screen_height": self.height,
-            "browser_language": self.browser_language.lower() or "en-us",
-            "browser_platform": "iPhone",
-            "browser_name": "Mozilla",
-            "browser_version": self.___format_new_params__(self.userAgent),
-            "browser_online": "true",
-            "timezone_name": self.timezone_name or "America/Chicago",
-            "is_page_visible": "true",
-            "focus_state": "true",
-            "is_fullscreen": "false",
-            "history_len": random.randint(0, 30),
-            "language": self.language or "en",
-        }
-        return urlencode(query)
-
-    def __extract_tag_contents(self, html):
-        nonce_start = '<head nonce="'
-        nonce_end = '">'
-        nonce = html.split(nonce_start)[1].split(nonce_end)[0]
-        j_raw = html.split(
-            '<script id="__NEXT_DATA__" type="application/json" nonce="%s" crossorigin="anonymous">'
-            % nonce
-        )[1].split("</script>")[0]
-        return j_raw
-
-    # Process the kwargs
-    def __process_kwargs__(self, kwargs):
-        region = kwargs.get("region", "US")
-        language = kwargs.get("language", "en")
-        proxy = kwargs.get("proxy", None)
-        maxCount = kwargs.get("maxCount", 35)
-
-        if kwargs.get("custom_device_id", None) != None:
-            device_id = kwargs.get("custom_device_id")
-        else:
-            if self.custom_device_id != None:
-                device_id = self.custom_device_id
-            else:
-                device_id = "".join(random.choice(string.digits) for num in range(19))
-        return region, language, proxy, maxCount, device_id
-
- -
- - - -
-
#   - - - TikTokApi(**kwargs) -
- -
- View Source -
    def __init__(self, **kwargs):
-        """The TikTokApi class. Used to interact with TikTok, use get_instance NOT this."""
-        # Forces Singleton
-        if TikTokApi.__instance is None:
-            TikTokApi.__instance = self
-        else:
-            raise Exception("Only one TikTokApi object is allowed")
-        logging.basicConfig(level=kwargs.get("logging_level", logging.WARNING))
-        logging.info("Class initalized")
-
-        # Some Instance Vars
-        self.executablePath = kwargs.get("executablePath", None)
-
-        if kwargs.get("custom_did") != None:
-            raise Exception("Please use custom_device_id instead of custom_device_id")
-        self.custom_device_id = kwargs.get("custom_device_id", None)
-        self.userAgent = (
-            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
-            "AppleWebKit/537.36 (KHTML, like Gecko) "
-            "Chrome/86.0.4240.111 Safari/537.36"
-        )
-        self.proxy = kwargs.get("proxy", None)
-        self.custom_verifyFp = kwargs.get("custom_verifyFp")
-        self.signer_url = kwargs.get("external_signer", None)
-        self.request_delay = kwargs.get("request_delay", None)
-        self.requests_extra_kwargs = kwargs.get("requests_extra_kwargs", {})
-
-        if kwargs.get("use_test_endpoints", False):
-            global BASE_URL
-            BASE_URL = "https://t.tiktok.com/"
-        if kwargs.get("use_selenium", False):
-            from .browser_utilities.browser_selenium import browser
-        else:
-            from .browser_utilities.browser import browser
-
-        if kwargs.get("generate_static_device_id", False):
-            self.custom_device_id = "".join(
-                random.choice(string.digits) for num in range(19)
-            )
-
-        if self.signer_url is None:
-            self.browser = browser(**kwargs)
-            self.userAgent = self.browser.userAgent
-
-        try:
-            self.timezone_name = self.___format_new_params__(self.browser.timezone_name)
-            self.browser_language = self.___format_new_params__(
-                self.browser.browser_language
-            )
-            self.width = self.browser.width
-            self.height = self.browser.height
-            self.region = self.browser.region
-            self.language = self.browser.language
-        except Exception as e:
-            logging.exception(e)
-            logging.warning(
-                "An error ocurred while opening your browser but it was ignored."
-            )
-            logging.warning("Are you sure you ran python -m playwright install")
-
-            self.timezone_name = ""
-            self.browser_language = ""
-            self.width = "1920"
-            self.height = "1080"
-            self.region = "US"
-            self.language = "en"
-
- -
- -

The TikTokApi class. Used to interact with TikTok, use get_instance NOT this.

-
- - -
-
-
#   - -
@staticmethod
- - def - get_instance(**kwargs): -
- -
- View Source -
    @staticmethod
-    def get_instance(**kwargs):
-        """The TikTokApi class. Used to interact with TikTok. This is a singleton
-            class to prevent issues from arising with playwright
-
-        ##### Parameters
-        * logging_level: The logging level you want the program to run at, optional
-            These are the standard python logging module's levels.
-
-        * request_delay: The amount of time in seconds to wait before making a request, optional
-            This is used to throttle your own requests as you may end up making too
-            many requests to TikTok for your IP.
-
-        * custom_device_id: A TikTok parameter needed to download videos, optional
-            The code generates these and handles these pretty well itself, however
-            for some things such as video download you will need to set a consistent
-            one of these. All the methods take this as a optional parameter, however
-            it's cleaner code to store this at the instance level. You can override
-            this at the specific methods.
-
-        * generate_static_device_id: A parameter that generates a custom_device_id at the instance level
-            Use this if you want to download videos from a script but don't want to generate
-            your own custom_device_id parameter.
-
-        * custom_verifyFp: A TikTok parameter needed to work most of the time, optional
-            To get this parameter look at [this video](https://youtu.be/zwLmLfVI-VQ?t=117)
-            I recommend watching the entire thing, as it will help setup this package. All
-            the methods take this as a optional parameter, however it's cleaner code
-            to store this at the instance level. You can override this at the specific
-            methods.
-
-            You can use the following to generate `"".join(random.choice(string.digits)
-            for num in range(19))`
-
-        * use_test_endpoints: Send requests to TikTok's test endpoints, optional
-            This parameter when set to true will make requests to TikTok's testing
-            endpoints instead of the live site. I can't guarantee this will work
-            in the future, however currently basically any custom_verifyFp will
-            work here which is helpful.
-
-        * proxy: A string containing your proxy address, optional
-            If you want to do a lot of scraping of TikTok endpoints you'll likely
-            need a proxy.
-
-            Ex: "https://0.0.0.0:8080"
-
-            All the methods take this as a optional parameter, however it's cleaner code
-            to store this at the instance level. You can override this at the specific
-            methods.
-
-        * use_selenium: Option to use selenium over playwright, optional
-            Playwright is selected by default and is the one that I'm designing the
-            package to be compatable for, however if playwright doesn't work on
-            your machine feel free to set this to True.
-
-        * executablePath: The location of the driver, optional
-            This shouldn't be needed if you're using playwright
-
-        * **kwargs
-            Parameters that are passed on to basically every module and methods
-            that interact with this main class. These may or may not be documented
-            in other places.
-        """
-        if not TikTokApi.__instance:
-            TikTokApi(**kwargs)
-        return TikTokApi.__instance
-
- -
- -

The TikTokApi class. Used to interact with TikTok. This is a singleton - class to prevent issues from arising with playwright

- -
Parameters
- -
    -
  • logging_level: The logging level you want the program to run at, optional -These are the standard python logging module's levels.

  • -
  • request_delay: The amount of time in seconds to wait before making a request, optional -This is used to throttle your own requests as you may end up making too -many requests to TikTok for your IP.

  • -
  • custom_device_id: A TikTok parameter needed to download videos, optional -The code generates these and handles these pretty well itself, however -for some things such as video download you will need to set a consistent -one of these. All the methods take this as a optional parameter, however -it's cleaner code to store this at the instance level. You can override -this at the specific methods.

  • -
  • generate_static_device_id: A parameter that generates a custom_device_id at the instance level -Use this if you want to download videos from a script but don't want to generate -your own custom_device_id parameter.

  • -
  • custom_verifyFp: A TikTok parameter needed to work most of the time, optional -To get this parameter look at this video -I recommend watching the entire thing, as it will help setup this package. All -the methods take this as a optional parameter, however it's cleaner code -to store this at the instance level. You can override this at the specific -methods.

    - -

    You can use the following to generate "".join(random.choice(string.digits) -for num in range(19))

  • -
  • use_test_endpoints: Send requests to TikTok's test endpoints, optional -This parameter when set to true will make requests to TikTok's testing -endpoints instead of the live site. I can't guarantee this will work -in the future, however currently basically any custom_verifyFp will -work here which is helpful.

  • -
  • proxy: A string containing your proxy address, optional -If you want to do a lot of scraping of TikTok endpoints you'll likely -need a proxy.

    - -

    Ex: "https://0.0.0.0:8080"

    - -

    All the methods take this as a optional parameter, however it's cleaner code -to store this at the instance level. You can override this at the specific -methods.

  • -
  • use_selenium: Option to use selenium over playwright, optional -Playwright is selected by default and is the one that I'm designing the -package to be compatable for, however if playwright doesn't work on -your machine feel free to set this to True.

  • -
  • executablePath: The location of the driver, optional -This shouldn't be needed if you're using playwright

  • -
  • **kwargs -Parameters that are passed on to basically every module and methods -that interact with this main class. These may or may not be documented -in other places.

  • -
-
- - -
-
-
#   - - - def - clean_up(self): -
- -
- View Source -
    def clean_up(self):
-        """A basic cleanup method, called automatically from the code"""
-        self.__del__()
-
- -
- -

A basic cleanup method, called automatically from the code

-
- - -
-
-
#   - - - def - external_signer(self, url, custom_device_id=None, verifyFp=None): -
- -
- View Source -
    def external_signer(self, url, custom_device_id=None, verifyFp=None):
-        """Makes requests to an external signer instead of using a browser.
-
-        ##### Parameters
-        * url: The server to make requests to
-            This server is designed to sign requests. You can find an example
-            of this signature server in the examples folder.
-
-        * custom_device_id: A TikTok parameter needed to download videos
-            The code generates these and handles these pretty well itself, however
-            for some things such as video download you will need to set a consistent
-            one of these.
-
-        * custom_verifyFp: A TikTok parameter needed to work most of the time,
-            To get this parameter look at [this video](https://youtu.be/zwLmLfVI-VQ?t=117)
-            I recommend watching the entire thing, as it will help setup this package.
-        """
-        if custom_device_id is not None:
-            query = {
-                "url": url,
-                "custom_device_id": custom_device_id,
-                "verifyFp": verifyFp,
-            }
-        else:
-            query = {"url": url, "verifyFp": verifyFp}
-        data = requests.get(
-            self.signer_url + "?{}".format(urlencode(query)),
-            **self.requests_extra_kwargs,
-        )
-        parsed_data = data.json()
-
-        return (
-            parsed_data["verifyFp"],
-            parsed_data["device_id"],
-            parsed_data["_signature"],
-            parsed_data["userAgent"],
-            parsed_data["referrer"],
-        )
-
- -
- -

Makes requests to an external signer instead of using a browser.

- -
Parameters
- -
    -
  • url: The server to make requests to -This server is designed to sign requests. You can find an example -of this signature server in the examples folder.

  • -
  • custom_device_id: A TikTok parameter needed to download videos -The code generates these and handles these pretty well itself, however -for some things such as video download you will need to set a consistent -one of these.

  • -
  • custom_verifyFp: A TikTok parameter needed to work most of the time, -To get this parameter look at this video -I recommend watching the entire thing, as it will help setup this package.

  • -
-
- - -
-
-
#   - - - def - get_data(self, **kwargs) -> dict: -
- -
- View Source -
    def get_data(self, **kwargs) -> dict:
-        """Makes requests to TikTok and returns their JSON.
-
-        This is all handled by the package so it's unlikely
-        you will need to use this.
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        if self.request_delay is not None:
-            time.sleep(self.request_delay)
-
-        if self.proxy is not None:
-            proxy = self.proxy
-
-        if kwargs.get("custom_verifyFp") == None:
-            if self.custom_verifyFp != None:
-                verifyFp = self.custom_verifyFp
-            else:
-                verifyFp = "verify_khr3jabg_V7ucdslq_Vrw9_4KPb_AJ1b_Ks706M8zIJTq"
-        else:
-            verifyFp = kwargs.get("custom_verifyFp")
-
-        tt_params = None
-        send_tt_params = kwargs.get("send_tt_params", False)
-
-        if self.signer_url is None:
-            kwargs["custom_verifyFp"] = verifyFp
-            verify_fp, device_id, signature, tt_params = self.browser.sign_url(
-                calc_tt_params=send_tt_params, **kwargs
-            )
-            userAgent = self.browser.userAgent
-            referrer = self.browser.referrer
-        else:
-            verify_fp, device_id, signature, userAgent, referrer = self.external_signer(
-                kwargs["url"],
-                custom_device_id=kwargs.get("custom_device_id"),
-                verifyFp=kwargs.get("custom_verifyFp", verifyFp),
-            )
-
-        if not kwargs.get("send_tt_params", False):
-            tt_params = None
-
-        query = {"verifyFp": verify_fp, "device_id": device_id, "_signature": signature}
-        url = "{}&{}".format(kwargs["url"], urlencode(query))
-
-        h = requests.head(
-            url,
-            headers={"x-secsdk-csrf-version": "1.2.5", "x-secsdk-csrf-request": "1"},
-            proxies=self.__format_proxy(proxy),
-            **self.requests_extra_kwargs,
-        )
-        csrf_session_id = h.cookies["csrf_session_id"]
-        csrf_token = h.headers["X-Ware-Csrf-Token"].split(",")[1]
-        kwargs["csrf_session_id"] = csrf_session_id
-
-        headers = {
-            "authority": "m.tiktok.com",
-            "method": "GET",
-            "path": url.split("tiktok.com")[1],
-            "scheme": "https",
-            "accept": "application/json, text/plain, */*",
-            "accept-encoding": "gzip, deflate, br",
-            "accept-language": "en-US,en;q=0.9",
-            "origin": referrer,
-            "referer": referrer,
-            "sec-fetch-dest": "empty",
-            "sec-fetch-mode": "cors",
-            "sec-fetch-site": "same-site",
-            "sec-gpc": "1",
-            "user-agent": userAgent,
-            "x-secsdk-csrf-token": csrf_token,
-            "x-tt-params": tt_params,
-        }
-
-        logging.info(f"GET: {url}\n\theaders: {headers}")
-        r = requests.get(
-            url,
-            headers=headers,
-            cookies=self.get_cookies(**kwargs),
-            proxies=self.__format_proxy(proxy),
-            **self.requests_extra_kwargs,
-        )
-        try:
-            json = r.json()
-            if (
-                json.get("type") == "verify"
-                or json.get("verifyConfig", {}).get("type", "") == "verify"
-            ):
-                logging.error(
-                    "Tiktok wants to display a catcha. Response is:\n" + r.text
-                )
-                logging.error(self.get_cookies(**kwargs))
-                raise TikTokCaptchaError()
-            
-            # statusCode from props->pageProps->statusCode thanks @adiantek on #403
-            error_codes = {
-                "0": "OK",
-                "450": "CLIENT_PAGE_ERROR",
-                "10000": "VERIFY_CODE",
-                "10101": "SERVER_ERROR_NOT_500",
-                "10102": "USER_NOT_LOGIN",
-                "10111": "NET_ERROR",
-                "10113": "SHARK_SLIDE",
-                "10114": "SHARK_BLOCK",
-                "10119": "LIVE_NEED_LOGIN",
-                "10202": "USER_NOT_EXIST",
-                "10203": "MUSIC_NOT_EXIST",
-                "10204": "VIDEO_NOT_EXIST",
-                "10205": "HASHTAG_NOT_EXIST",
-                "10208": "EFFECT_NOT_EXIST",
-                "10209": "HASHTAG_BLACK_LIST",
-                "10210": "LIVE_NOT_EXIST",
-                "10211": "HASHTAG_SENSITIVITY_WORD",
-                "10212": "HASHTAG_UNSHELVE",
-                "10213": "VIDEO_LOW_AGE_M",
-                "10214": "VIDEO_LOW_AGE_T",
-                "10215": "VIDEO_ABNORMAL",
-                "10216": "VIDEO_PRIVATE_BY_USER",
-                "10217": "VIDEO_FIRST_REVIEW_UNSHELVE",
-                "10218": "MUSIC_UNSHELVE",
-                "10219": "MUSIC_NO_COPYRIGHT",
-                "10220": "VIDEO_UNSHELVE_BY_MUSIC",
-                "10221": "USER_BAN",
-                "10222": "USER_PRIVATE",
-                "10223": "USER_FTC",
-                "10224": "GAME_NOT_EXIST",
-                "10225": "USER_UNIQUE_SENSITIVITY",
-                "10227": "VIDEO_NEED_RECHECK",
-                "10228": "VIDEO_RISK",
-                "10229": "VIDEO_R_MASK",
-                "10230": "VIDEO_RISK_MASK",
-                "10231": "VIDEO_GEOFENCE_BLOCK",
-                "10404": "FYP_VIDEO_LIST_LIMIT",
-                "undefined": "MEDIA_ERROR"
-            }
-            statusCode = json.get("statusCode", 0)
-            logging.info(f"TikTok Returned: {json}")
-            if statusCode == 10201:
-                # Invalid Entity
-                raise TikTokNotFoundError(
-                    "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")
-            elif statusCode != 0 and statusCode != -1:
-                raise GenericTikTokError(error_codes.get(statusCode, f"TikTok sent an unknown StatusCode of {statusCode}"))
-
-            return r.json()
-        except ValueError as e:
-            text = r.text
-            logging.error("TikTok response: " + text)
-            if len(text) == 0:
-                raise EmptyResponseError(
-                    "Empty response from Tiktok to " + url
-                ) from None
-            else:
-                logging.error("Converting response to JSON failed")
-                logging.error(e)
-                raise JSONDecodeFailure() from e
-
- -
- -

Makes requests to TikTok and returns their JSON.

- -

This is all handled by the package so it's unlikely -you will need to use this.

-
- - -
-
-
#   - - - def - get_cookies(self, **kwargs): -
- -
- View Source -
    def get_cookies(self, **kwargs):
-        """Extracts cookies from the kwargs passed to the function for get_data"""
-        device_id = kwargs.get(
-            "custom_device_id",
-            "".join(random.choice(string.digits) for num in range(19)),
-        )
-        if kwargs.get("custom_verifyFp") == None:
-            if self.custom_verifyFp != None:
-                verifyFp = self.custom_verifyFp
-            else:
-                verifyFp = "verify_khr3jabg_V7ucdslq_Vrw9_4KPb_AJ1b_Ks706M8zIJTq"
-        else:
-            verifyFp = kwargs.get("custom_verifyFp")
-
-        if kwargs.get("force_verify_fp_on_cookie_header", False):
-            return {
-                "tt_webid": device_id,
-                "tt_webid_v2": device_id,
-                "csrf_session_id": kwargs.get("csrf_session_id"),
-                "tt_csrf_token": "".join(
-                    random.choice(string.ascii_uppercase + string.ascii_lowercase)
-                    for i in range(16)
-                ),
-                "s_v_web_id": verifyFp,
-                "ttwid": kwargs.get("ttwid")
-            }
-        else:
-            return {
-                "tt_webid": device_id,
-                "tt_webid_v2": device_id,
-                "csrf_session_id": kwargs.get("csrf_session_id"),
-                "tt_csrf_token": "".join(
-                    random.choice(string.ascii_uppercase + string.ascii_lowercase)
-                    for i in range(16)
-                ),
-                "ttwid": kwargs.get("ttwid")
-            }
-
- -
- -

Extracts cookies from the kwargs passed to the function for get_data

-
- - -
-
-
#   - - - def - get_bytes(self, **kwargs) -> bytes: -
- -
- View Source -
    def get_bytes(self, **kwargs) -> bytes:
-        """Returns TikTok's response as bytes, similar to get_data"""
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        if self.signer_url is None:
-            verify_fp, device_id, signature, _ = self.browser.sign_url(
-                calc_tt_params=False, **kwargs
-            )
-            userAgent = self.browser.userAgent
-            referrer = self.browser.referrer
-        else:
-            verify_fp, device_id, signature, userAgent, referrer = self.external_signer(
-                kwargs["url"], custom_device_id=kwargs.get("custom_device_id", None)
-            )
-        query = {"verifyFp": verify_fp, "_signature": signature}
-        url = "{}&{}".format(kwargs["url"], urlencode(query))
-        r = requests.get(
-            url,
-            headers={
-                "Accept": "*/*",
-                "Accept-Encoding": "identity;q=1, *;q=0",
-                "Accept-Language": "en-US;en;q=0.9",
-                "Cache-Control": "no-cache",
-                "Connection": "keep-alive",
-                "Host": url.split("/")[2],
-                "Pragma": "no-cache",
-                "Range": "bytes=0-",
-                "Referer": "https://www.tiktok.com/",
-                "User-Agent": userAgent,
-            },
-            proxies=self.__format_proxy(proxy),
-            cookies=self.get_cookies(**kwargs),
-        )
-        return r.content
-
- -
- -

Returns TikTok's response as bytes, similar to get_data

-
- - -
- -
-
#   - - - def - search_for_users(self, search_term, count=28, **kwargs) -> list: -
- -
- View Source -
    def search_for_users(self, search_term, count=28, **kwargs) -> list:
-        """Returns a list of users that match the search_term
-
-        ##### Parameters
-        * search_term: The string to search for users by
-            This string is the term you want to search for users by.
-
-        * count: The number of users to return
-            Note: maximum is around 28 for this type of endpoint.
-        """
-        return self.discover_type(search_term, prefix="user", count=count, **kwargs)
-
- -
- -

Returns a list of users that match the search_term

- -
Parameters
- -
    -
  • search_term: The string to search for users by -This string is the term you want to search for users by.

  • -
  • count: The number of users to return -Note: maximum is around 28 for this type of endpoint.

  • -
-
- - -
-
-
#   - - - def - search_for_music(self, search_term, count=28, **kwargs) -> list: -
- -
- View Source -
    def search_for_music(self, search_term, count=28, **kwargs) -> list:
-        """Returns a list of music that match the search_term
-
-        ##### Parameters
-        * search_term: The string to search for music by
-            This string is the term you want to search for music by.
-
-        * count: The number of music to return
-            Note: maximum is around 28 for this type of endpoint.
-        """
-        return self.discover_type(search_term, prefix="music", count=count, **kwargs)
-
- -
- -

Returns a list of music that match the search_term

- -
Parameters
- -
    -
  • search_term: The string to search for music by -This string is the term you want to search for music by.

  • -
  • count: The number of music to return -Note: maximum is around 28 for this type of endpoint.

  • -
-
- - -
-
-
#   - - - def - search_for_hashtags(self, search_term, count=28, **kwargs) -> list: -
- -
- View Source -
    def search_for_hashtags(self, search_term, count=28, **kwargs) -> list:
-        """Returns a list of hashtags that match the search_term
-
-        ##### Parameters
-        * search_term: The string to search for music by
-            This string is the term you want to search for music by.
-
-        * count: The number of music to return
-            Note: maximum is around 28 for this type of endpoint.
-        """
-        return self.discover_type(
-            search_term, prefix="challenge", count=count, **kwargs
-        )
-
- -
- -

Returns a list of hashtags that match the search_term

- -
Parameters
- -
    -
  • search_term: The string to search for music by -This string is the term you want to search for music by.

  • -
  • count: The number of music to return -Note: maximum is around 28 for this type of endpoint.

  • -
-
- - -
-
-
#   - - - def - discover_type(self, search_term, prefix, count=28, offset=0, **kwargs) -> list: -
- -
- View Source -
    def discover_type(self, search_term, prefix, count=28, offset=0, **kwargs) -> list:
-        """Returns a list of whatever the prefix type you pass in
-
-        ##### Parameters
-        * search_term: The string to search by
-
-        * prefix: The prefix of what to search for
-
-        * count: The number search results to return
-            Note: maximum is around 28 for this type of endpoint.
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        response = []
-        while len(response) < count:
-            query = {
-                "discoverType": 0,
-                "needItemList": False,
-                "keyWord": search_term,
-                "offset": offset,
-                "count": count,
-                "useRecommend": False,
-                "language": "en",
-            }
-            api_url = "{}api/discover/{}/?{}&{}".format(
-                BASE_URL, prefix, self.__add_url_params__(), urlencode(query)
-            )
-            data = self.get_data(url=api_url, **kwargs)
-
-            if "userInfoList" in data.keys():
-                for x in data["userInfoList"]:
-                    response.append(x)
-            elif "musicInfoList" in data.keys():
-                for x in data["musicInfoList"]:
-                    response.append(x)
-            elif "challengeInfoList" in data.keys():
-                for x in data["challengeInfoList"]:
-                    response.append(x)
-            else:
-                logging.info("TikTok is not sending videos beyond this point.")
-                break
-
-            offset += maxCount
-
-        return response[:count]
-
- -
- -

Returns a list of whatever the prefix type you pass in

- -
Parameters
- -
    -
  • search_term: The string to search by

  • -
  • prefix: The prefix of what to search for

  • -
  • count: The number search results to return -Note: maximum is around 28 for this type of endpoint.

  • -
-
- - -
-
-
#   - - - def - user_posts(self, userID, secUID, count=30, cursor=0, **kwargs) -> dict: -
- -
- View Source -
    def user_posts(self, userID, secUID, count=30, cursor=0, **kwargs) -> dict:
-        """Returns an array of dictionaries representing TikToks for a user.
-
-        ##### Parameters
-        * userID: The userID of the user, which TikTok assigns
-
-            You can find this from utilizing other methods or
-            just use by_username to find it.
-        * secUID: The secUID of the user, which TikTok assigns
-
-            You can find this from utilizing other methods or
-            just use by_username to find it.
-        * count: The number of posts to return
-
-            Note: seems to only support up to ~2,000
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        response = []
-        first = True
-
-        while len(response) < count:
-            if count < maxCount:
-                realCount = count
-            else:
-                realCount = maxCount
-
-            query = {
-                "count": realCount,
-                "id": userID,
-                "cursor": cursor,
-                "type": 1,
-                "secUid": secUID,
-                "sourceType": 8,
-                "appId": 1233,
-                "region": region,
-                "priority_region": region,
-                "language": language,
-            }
-            api_url = "{}api/post/item_list/?{}&{}".format(
-                BASE_URL, self.__add_url_params__(), urlencode(query)
-            )
-
-            res = self.get_data(url=api_url, send_tt_params=True, **kwargs)
-
-            if "itemList" in res.keys():
-                for t in res.get("itemList", []):
-                    response.append(t)
-
-            if not res.get("hasMore", False) and not first:
-                logging.info("TikTok isn't sending more TikToks beyond this point.")
-                return response
-
-            realCount = count - len(response)
-            cursor = res["cursor"]
-
-            first = False
-
-        return response[:count]
-
- -
- -

Returns an array of dictionaries representing TikToks for a user.

- -
Parameters
- -
    -
  • userID: The userID of the user, which TikTok assigns

    - -

    You can find this from utilizing other methods or -just use by_username to find it.

  • -
  • secUID: The secUID of the user, which TikTok assigns

    - -

    You can find this from utilizing other methods or -just use by_username to find it.

  • -
  • count: The number of posts to return

    - -

    Note: seems to only support up to ~2,000

  • -
-
- - -
-
-
#   - - - def - by_username(self, username, count=30, **kwargs) -> dict: -
- -
- View Source -
    def by_username(self, username, count=30, **kwargs) -> dict:
-        """Returns a dictionary listing TikToks given a user's username.
-
-        ##### Parameters
-        * username: The username of the TikTok user to get TikToks from
-
-        * count: The number of posts to return
-
-            Note: seems to only support up to ~2,000
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        data = self.get_user_object(username, **kwargs)
-        return self.user_posts(
-            data["id"],
-            data["secUid"],
-            count=count,
-            **kwargs,
-        )
-
- -
- -

Returns a dictionary listing TikToks given a user's username.

- -
Parameters
- -
    -
  • username: The username of the TikTok user to get TikToks from

  • -
  • count: The number of posts to return

    - -

    Note: seems to only support up to ~2,000

  • -
-
- - -
-
-
#   - - - def - user_page(self, userID, secUID, page_size=30, cursor=0, **kwargs) -> dict: -
- -
- View Source -
    def user_page(self, userID, secUID, page_size=30, cursor=0, **kwargs) -> dict:
-        """Returns a dictionary listing of one page of TikToks given a user's ID and secUID
-
-        ##### Parameters
-        * userID: The userID of the user, which TikTok assigns
-
-            You can find this from utilizing other methods or
-            just use by_username to find it.
-        * secUID: The secUID of the user, which TikTok assigns
-
-            You can find this from utilizing other methods or
-            just use by_username to find it.
-        * page_size: The number of posts to return per page
-
-            Gets a specific page of a user, doesn't iterate.
-        * cursor: The offset of a page
-
-            The offset to return new videos from
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        api_url = (
-            BASE_URL + "api/post/item_list/?{}&count={}&id={}&type=1&secUid={}"
-            "&cursor={}&sourceType=8&appId=1233&region={}&language={}".format(
-                self.__add_url_params__(),
-                page_size,
-                str(userID),
-                str(secUID),
-                cursor,
-                region,
-                language,
-            )
-        )
-
-        return self.get_data(url=api_url, send_tt_params=True, **kwargs)
-
- -
- -

Returns a dictionary listing of one page of TikToks given a user's ID and secUID

- -
Parameters
- -
    -
  • userID: The userID of the user, which TikTok assigns

    - -

    You can find this from utilizing other methods or -just use by_username to find it.

  • -
  • secUID: The secUID of the user, which TikTok assigns

    - -

    You can find this from utilizing other methods or -just use by_username to find it.

  • -
  • page_size: The number of posts to return per page

    - -

    Gets a specific page of a user, doesn't iterate.

  • -
  • cursor: The offset of a page

    - -

    The offset to return new videos from

  • -
-
- - -
-
-
#   - - - def - get_user_pager(self, username, page_size=30, cursor=0, **kwargs): -
- -
- View Source -
    def get_user_pager(self, username, page_size=30, cursor=0, **kwargs):
-        """Returns a generator to page through a user's feed
-
-        ##### Parameters
-        * username: The username of the user
-
-        * page_size: The number of posts to return in a page
-
-        * cursor: The offset of a page
-
-            The offset to return new videos from
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        data = self.get_user_object(username, **kwargs)
-
-        while True:
-            resp = self.user_page(
-                data["id"],
-                data["secUid"],
-                page_size=page_size,
-                cursor=cursor,
-                **kwargs,
-            )
-
-            try:
-                page = resp["itemList"]
-            except KeyError:
-                # No mo results
-                return
-
-            cursor = resp["cursor"]
-
-            yield page
-
-            if not resp["hasMore"]:
-                return  # all done
-
- -
- -

Returns a generator to page through a user's feed

- -
Parameters
- -
    -
  • username: The username of the user

  • -
  • page_size: The number of posts to return in a page

  • -
  • cursor: The offset of a page

    - -

    The offset to return new videos from

  • -
-
- - -
-
-
#   - - - def - user_liked(self, userID, secUID, count=30, cursor=0, **kwargs) -> dict: -
- -
- View Source -
    def user_liked(self, userID, secUID, count=30, cursor=0, **kwargs) -> dict:
-        """Returns a dictionary listing TikToks that a given a user has liked.
-            Note: The user's likes must be public
-
-        ##### Parameters
-        * userID: The userID of the user, which TikTok assigns
-
-        * secUID: The secUID of the user, which TikTok assigns
-
-        * count: The number of posts to return
-
-            Note: seems to only support up to ~2,000
-        * cursor: The offset of a page
-
-            The offset to return new videos from
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        response = []
-        first = True
-
-        while len(response) < count:
-            if count < maxCount:
-                realCount = count
-            else:
-                realCount = maxCount
-
-            query = {
-                "count": realCount,
-                "id": userID,
-                "type": 2,
-                "secUid": secUID,
-                "cursor": cursor,
-                "sourceType": 9,
-                "appId": 1233,
-                "region": region,
-                "priority_region": region,
-                "language": language,
-            }
-            api_url = "{}api/favorite/item_list/?{}&{}".format(
-                BASE_URL, self.__add_url_params__(), urlencode(query)
-            )
-
-            res = self.get_data(url=api_url, **kwargs)
-
-            try:
-                res["itemList"]
-            except Exception:
-                logging.error("User's likes are most likely private")
-                return []
-
-            for t in res.get("itemList", []):
-                response.append(t)
-
-            if not res.get("hasMore", False) and not first:
-                logging.info("TikTok isn't sending more TikToks beyond this point.")
-                return response
-
-            realCount = count - len(response)
-            cursor = res["cursor"]
-
-            first = False
-
-        return response[:count]
-
- -
- -

Returns a dictionary listing TikToks that a given a user has liked. - Note: The user's likes must be public

- -
Parameters
- -
    -
  • userID: The userID of the user, which TikTok assigns

  • -
  • secUID: The secUID of the user, which TikTok assigns

  • -
  • count: The number of posts to return

    - -

    Note: seems to only support up to ~2,000

  • -
  • cursor: The offset of a page

    - -

    The offset to return new videos from

  • -
-
- - -
-
-
#   - - - def - user_liked_by_username(self, username, count=30, **kwargs) -> dict: -
- -
- View Source -
    def user_liked_by_username(self, username, count=30, **kwargs) -> dict:
-        """Returns a dictionary listing TikToks a user has liked by username.
-            Note: The user's likes must be public
-
-        ##### Parameters
-        * username: The username of the user
-
-        * count: The number of posts to return
-
-            Note: seems to only support up to ~2,000
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        data = self.get_user_object(username, **kwargs)
-        return self.user_liked(
-            data["id"],
-            data["secUid"],
-            count=count,
-            **kwargs,
-        )
-
- -
- -

Returns a dictionary listing TikToks a user has liked by username. - Note: The user's likes must be public

- -
Parameters
- -
    -
  • username: The username of the user

  • -
  • count: The number of posts to return

    - -

    Note: seems to only support up to ~2,000

  • -
-
- - -
-
-
#   - - - def - by_sound(self, id, count=30, offset=0, **kwargs) -> dict: -
- -
- View Source -
    def by_sound(self, id, count=30, offset=0, **kwargs) -> dict:
-        """Returns a dictionary listing TikToks with a specific sound.
-
-        ##### Parameters
-        * id: The sound id to search by
-
-            Note: Can be found in the URL of the sound specific page or with other methods.
-        * count: The number of posts to return
-
-            Note: seems to only support up to ~2,000
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        response = []
-
-        while len(response) < count:
-            if count < maxCount:
-                realCount = count
-            else:
-                realCount = maxCount
-
-            query = {
-                "secUid": "",
-                "musicID": str(id),
-                "count": str(realCount),
-                "cursor": offset,
-                "shareUid": "",
-                "language": language,
-            }
-            api_url = "{}api/music/item_list/?{}&{}".format(
-                BASE_URL, self.__add_url_params__(), urlencode(query)
-            )
-
-            res = self.get_data(url=api_url, send_tt_params=True, **kwargs)
-
-            try:
-                for t in res["items"]:
-                    response.append(t)
-            except KeyError:
-                for t in res.get("itemList", []):
-                    response.append(t)
-
-            if not res.get("hasMore", False):
-                logging.info("TikTok isn't sending more TikToks beyond this point.")
-                return response
-
-            realCount = count - len(response)
-            offset = res["cursor"]
-
-        return response[:count]
-
- -
- -

Returns a dictionary listing TikToks with a specific sound.

- -
Parameters
- -
    -
  • id: The sound id to search by

    - -

    Note: Can be found in the URL of the sound specific page or with other methods.

  • -
  • count: The number of posts to return

    - -

    Note: seems to only support up to ~2,000

  • -
-
- - -
-
-
#   - - - def - by_sound_page(self, id, page_size=30, cursor=0, **kwargs) -> dict: -
- -
- View Source -
    def by_sound_page(self, id, page_size=30, cursor=0, **kwargs) -> dict:
-        """Returns a page of tiktoks with a specific sound.
-
-        Parameters
-        ----------
-        id: The sound id to search by
-            Note: Can be found in the URL of the sound specific page or with other methods.
-        cursor: offset for pagination
-        page_size: The number of posts to return
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        query = {
-            "musicID": str(id),
-            "count": str(page_size),
-            "cursor": cursor,
-            "language": language,
-        }
-        api_url = "{}api/music/item_list/?{}&{}".format(
-            BASE_URL, self.__add_url_params__(), urlencode(query)
-        )
-
-        return self.get_data(url=api_url, send_tt_params=True, **kwargs)
-
- -
- -

Returns a page of tiktoks with a specific sound.

- -

Parameters

- -

id: The sound id to search by - Note: Can be found in the URL of the sound specific page or with other methods. -cursor: offset for pagination -page_size: The number of posts to return

-
- - -
-
-
#   - - - def - get_music_object(self, id, **kwargs) -> dict: -
- -
- View Source -
    def get_music_object(self, id, **kwargs) -> dict:
-        """Returns a music object for a specific sound id.
-
-        ##### Parameters
-        * id: The sound id to get the object for
-
-            This can be found by using other methods.
-        """
-        return self.get_music_object_full(id, **kwargs)["music"]
-
- -
- -

Returns a music object for a specific sound id.

- -
Parameters
- -
    -
  • id: The sound id to get the object for

    - -

    This can be found by using other methods.

  • -
-
- - -
-
-
#   - - - def - get_music_object_full(self, id, **kwargs): -
- -
- View Source -
    def get_music_object_full(self, id, **kwargs):
-        """Returns a music object for a specific sound id.
-
-        ##### Parameters
-        * id: The sound id to get the object for
-
-            This can be found by using other methods.
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        r = requests.get(
-            "https://www.tiktok.com/music/-{}".format(id),
-            headers={
-                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
-                "Accept-Encoding": "gzip, deflate",
-                "Connection": "keep-alive",
-                "User-Agent": self.userAgent,
-            },
-            proxies=self.__format_proxy(kwargs.get("proxy", None)),
-            cookies=self.get_cookies(**kwargs),
-            **self.requests_extra_kwargs,
-        )
-
-        j_raw = self.__extract_tag_contents(r.text)
-        return json.loads(j_raw)["props"]["pageProps"]["musicInfo"]
-
- -
- -

Returns a music object for a specific sound id.

- -
Parameters
- -
    -
  • id: The sound id to get the object for

    - -

    This can be found by using other methods.

  • -
-
- - -
-
-
#   - - - def - get_music_object_full_by_api(self, id, **kwargs): -
- -
- View Source -
    def get_music_object_full_by_api(self, id, **kwargs):
-        """Returns a music object for a specific sound id, but using the API rather than HTML requests.
-
-        ##### Parameters
-        * id: The sound id to get the object for
-
-            This can be found by using other methods.
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        api_url = "{}node/share/music/-{}?{}".format(
-            BASE_URL, id, self.__add_url_params__()
-        )
-        res = self.get_data(url=api_url, **kwargs)
-
-        if res.get("statusCode", 200) == 10203:
-            raise TikTokNotFoundError()
-
-        return res["musicInfo"]
-
- -
- -

Returns a music object for a specific sound id, but using the API rather than HTML requests.

- -
Parameters
- -
    -
  • id: The sound id to get the object for

    - -

    This can be found by using other methods.

  • -
-
- - -
-
-
#   - - - def - by_hashtag(self, hashtag, count=30, offset=0, **kwargs) -> dict: -
- -
- View Source -
    def by_hashtag(self, hashtag, count=30, offset=0, **kwargs) -> dict:
-        """Returns a dictionary listing TikToks with a specific hashtag.
-
-        ##### Parameters
-        * hashtag: The hashtag to search by
-
-            Without the # symbol
-
-            A valid string is "funny"
-        * count: The number of posts to return
-            Note: seems to only support up to ~2,000
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        id = self.get_hashtag_object(hashtag)["challengeInfo"]["challenge"]["id"]
-        response = []
-
-        required_count = count
-
-        while len(response) < required_count:
-            if count > maxCount:
-                count = maxCount
-            query = {
-                "count": count,
-                "challengeID": id,
-                "type": 3,
-                "secUid": "",
-                "cursor": offset,
-                "priority_region": "",
-            }
-            api_url = "{}api/challenge/item_list/?{}&{}".format(
-                BASE_URL, self.__add_url_params__(), urlencode(query)
-            )
-            res = self.get_data(url=api_url, **kwargs)
-
-            for t in res.get("itemList", []):
-                response.append(t)
-
-            if not res.get("hasMore", False):
-                logging.info("TikTok isn't sending more TikToks beyond this point.")
-                return response
-
-            offset += maxCount
-
-        return response[:required_count]
-
- -
- -

Returns a dictionary listing TikToks with a specific hashtag.

- -
Parameters
- -
    -
  • hashtag: The hashtag to search by

    - -

    Without the # symbol

    - -

    A valid string is "funny"

  • -
  • count: The number of posts to return -Note: seems to only support up to ~2,000
  • -
-
- - -
-
-
#   - - - def - get_hashtag_object(self, hashtag, **kwargs) -> dict: -
- -
- View Source -
    def get_hashtag_object(self, hashtag, **kwargs) -> dict:
-        """Returns a hashtag object.
-
-        ##### Parameters
-        * hashtag: The hashtag to search by
-
-            Without the # symbol
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        query = {"name": hashtag, "isName": True, "lang": language}
-        api_url = "{}node/share/tag/{}?{}&{}".format(
-            BASE_URL, quote(hashtag), self.__add_url_params__(), urlencode(query)
-        )
-        data = self.get_data(url=api_url, **kwargs)
-        if data["challengeInfo"].get("challenge") is None:
-            raise TikTokNotFoundError("Challenge {} does not exist".format(hashtag))
-        return data
-
- -
- -

Returns a hashtag object.

- -
Parameters
- -
    -
  • hashtag: The hashtag to search by

    - -

    Without the # symbol

  • -
-
- - -
- -
-
#   - - - def - get_tiktok_by_id(self, id, **kwargs) -> dict: -
- -
- View Source -
    def get_tiktok_by_id(self, id, **kwargs) -> dict:
-        """Returns a dictionary of a specific TikTok.
-
-        ##### Parameters
-        * id: The id of the TikTok you want to get the object for
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        device_id = kwargs.get("custom_device_id", None)
-        query = {
-            "itemId": id,
-            "language": language,
-        }
-        api_url = "{}api/item/detail/?{}&{}".format(
-            BASE_URL, self.__add_url_params__(), urlencode(query)
-        )
-
-        return self.get_data(url=api_url, **kwargs)
-
- -
- -

Returns a dictionary of a specific TikTok.

- -
Parameters
- -
    -
  • id: The id of the TikTok you want to get the object for
  • -
-
- - -
-
-
#   - - - def - get_tiktok_by_url(self, url, **kwargs) -> dict: -
- -
- View Source -
    def get_tiktok_by_url(self, url, **kwargs) -> dict:
-        """Returns a dictionary of a TikTok object by url.
-
-
-        ##### Parameters
-        * url: The TikTok url you want to retrieve
-
-        """
-
-        url = requests.head(url=url, allow_redirects=True).url
-
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        custom_device_id = kwargs.get("custom_device_id", None)
-        if "@" in url and "/video/" in url:
-            post_id = url.split("/video/")[1].split("?")[0]
-        else:
-            raise Exception(
-                "URL format not supported. Below is an example of a supported url.\n"
-                "https://www.tiktok.com/@therock/video/6829267836783971589"
-            )
-
-        return self.get_tiktok_by_id(
-            post_id,
-            **kwargs,
-        )
-
- -
- -

Returns a dictionary of a TikTok object by url.

- -
Parameters
- -
    -
  • url: The TikTok url you want to retrieve
  • -
-
- - -
-
-
#   - - - def - get_tiktok_by_html(self, url, **kwargs) -> dict: -
- -
- View Source -
    def get_tiktok_by_html(self, url, **kwargs) -> dict:
-        """This method retrieves a TikTok using the html
-        endpoints rather than the API based ones.
-
-        ##### Parameters
-        * url: The url of the TikTok to get
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-
-        r = requests.get(
-            url,
-            headers={
-                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
-                "path": url.split("tiktok.com")[1],
-                "Accept-Encoding": "gzip, deflate",
-                "Connection": "keep-alive",
-                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
-            },
-            proxies=self.__format_proxy(kwargs.get("proxy", None)),
-            cookies=self.get_cookies(**kwargs),
-            **self.requests_extra_kwargs,
-        )
-
-        t = r.text
-        try:
-            j_raw = self.__extract_tag_contents(r.text)
-        except IndexError:
-            if not t:
-                logging.error("TikTok response is empty")
-            else:
-                logging.error("TikTok response: \n " + t)
-            raise TikTokCaptchaError()
-
-        data = json.loads(j_raw)["props"]["pageProps"]
-
-        if data["serverCode"] == 404:
-            raise TikTokNotFoundError("TikTok with that url doesn't exist")
-
-        return data
-
- -
- -

This method retrieves a TikTok using the html -endpoints rather than the API based ones.

- -
Parameters
- -
    -
  • url: The url of the TikTok to get
  • -
-
- - -
-
-
#   - - - def - get_user_object(self, username, **kwargs) -> dict: -
- -
- View Source -
    def get_user_object(self, username, **kwargs) -> dict:
-        """Gets a user object (dictionary)
-
-        ##### Parameters
-        * username: The username of the user
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        return self.get_user(username, **kwargs)["userInfo"]["user"]
-
- -
- -

Gets a user object (dictionary)

- -
Parameters
- -
    -
  • username: The username of the user
  • -
-
- - -
-
-
#   - - - def - get_user(self, username, **kwargs) -> dict: -
- -
- View Source -
    def get_user(self, username, **kwargs) -> dict:
-        """Gets the full exposed user object
-
-        ##### Parameters
-        * username: The username of the user
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        r = requests.get(
-            "https://tiktok.com/@{}?lang=en".format(quote(username)),
-            headers={
-                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
-                "path": "/@{}".format(quote(username)),
-                "Accept-Encoding": "gzip, deflate",
-                "Connection": "keep-alive",
-                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
-            },
-            proxies=self.__format_proxy(kwargs.get("proxy", None)),
-            cookies=self.get_cookies(**kwargs),
-            **self.requests_extra_kwargs,
-        )
-
-        t = r.text
-
-        try:
-            j_raw = self.__extract_tag_contents(r.text)
-        except IndexError:
-            if not t:
-                logging.error("Tiktok response is empty")
-            else:
-                logging.error("Tiktok response: \n " + t)
-            raise TikTokCaptchaError()
-
-        user = json.loads(j_raw)["props"]["pageProps"]
-
-        if user["serverCode"] == 404:
-            raise TikTokNotFoundError(
-                "TikTok user with username {} does not exist".format(username)
-            )
-
-        return user
-
- -
- -

Gets the full exposed user object

- -
Parameters
- -
    -
  • username: The username of the user
  • -
-
- - -
-
-
#   - - - def - get_video_by_tiktok(self, data, **kwargs) -> bytes: -
- -
- View Source -
    def get_video_by_tiktok(self, data, **kwargs) -> bytes:
-        """Downloads video from TikTok using a TikTok object.
-
-        You will need to set a custom_device_id to do this for anything but trending.
-        To do this, this is pretty simple you can either generate one yourself or,
-        you can pass the generate_static_device_id=True into the constructor of the
-        TikTokApi class.
-
-        ##### Parameters
-        * data: A TikTok object
-
-            A TikTok JSON object from any other method.
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        try:
-            api_url = data["video"]["downloadAddr"]
-        except Exception:
-            try:
-                api_url = data["itemInfos"]["video"]["urls"][0]
-            except Exception:
-                api_url = data["itemInfo"]["itemStruct"]["video"]["playAddr"]
-        return self.get_video_by_download_url(api_url, **kwargs)
-
- -
- -

Downloads video from TikTok using a TikTok object.

- -

You will need to set a custom_device_id to do this for anything but trending. -To do this, this is pretty simple you can either generate one yourself or, -you can pass the generate_static_device_id=True into the constructor of the -TikTokApi class.

- -
Parameters
- -
    -
  • data: A TikTok object

    - -

    A TikTok JSON object from any other method.

  • -
-
- - -
-
-
#   - - - def - get_video_by_download_url(self, download_url, **kwargs) -> bytes: -
- -
- View Source -
    def get_video_by_download_url(self, download_url, **kwargs) -> bytes:
-        """Downloads video from TikTok using download url in a TikTok object
-
-        ##### Parameters
-        * download_url: The download url key value in a TikTok object
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-        return self.get_bytes(url=download_url, **kwargs)
-
- -
- -

Downloads video from TikTok using download url in a TikTok object

- -
Parameters
- -
    -
  • download_url: The download url key value in a TikTok object
  • -
-
- - -
-
-
#   - - - def - get_video_by_url(self, video_url, **kwargs) -> bytes: -
- -
- View Source -
    def get_video_by_url(self, video_url, **kwargs) -> bytes:
-        """Downloads a TikTok video by a URL
-
-        ##### Parameters
-        * video_url: The TikTok url to download the video from
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        kwargs["custom_device_id"] = device_id
-
-        tiktok_schema = self.get_tiktok_by_url(video_url, **kwargs)
-        download_url = tiktok_schema["itemInfo"]["itemStruct"]["video"]["downloadAddr"]
-
-        return self.get_bytes(url=download_url, **kwargs)
-
- -
- -

Downloads a TikTok video by a URL

- -
Parameters
- -
    -
  • video_url: The TikTok url to download the video from
  • -
-
- - -
-
-
#   - - - def - get_video_no_watermark(self, video_url, return_bytes=1, **kwargs) -> bytes: -
- -
- View Source -
    def get_video_no_watermark(self, video_url, return_bytes=1, **kwargs) -> bytes:
-        """Gets the video with no watermark
-        .. deprecated::
-
-        Deprecated due to TikTok fixing this
-
-        ##### Parameters
-        * video_url: The url of the video you want to download
-
-        * return_bytes: Set this to 0 if you want url, 1 if you want bytes
-        """
-        (
-            region,
-            language,
-            proxy,
-            maxCount,
-            device_id,
-        ) = self.__process_kwargs__(kwargs)
-        raise Exception("Deprecated method, TikTok fixed this.")
-
- -
- -

Gets the video with no watermark -.. deprecated::

- -

Deprecated due to TikTok fixing this

- -
Parameters
- -
    -
  • video_url: The url of the video you want to download

  • -
  • return_bytes: Set this to 0 if you want url, 1 if you want bytes

  • -
-
- - -
-
-
#   - - - def - get_music_title(self, id, **kwargs): -
- -
- View Source -
    def get_music_title(self, id, **kwargs):
-        """Retrieves a music title given an ID
-
-        ##### Parameters
-        * id: The music id to get the title for
-        """
-        r = requests.get(
-            "https://www.tiktok.com/music/-{}".format(id),
-            headers={
-                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
-                "Accept-Encoding": "gzip, deflate",
-                "Connection": "keep-alive",
-                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
-            },
-            proxies=self.__format_proxy(kwargs.get("proxy", None)),
-            cookies=self.get_cookies(**kwargs),
-            **self.requests_extra_kwargs,
-        )
-        t = r.text
-        j_raw = self.__extract_tag_contents(r.text)
-
-        music_object = json.loads(j_raw)["props"]["pageProps"]["musicInfo"]
-        if not music_object.get("title", None):
-            raise TikTokNotFoundError("Song of {} id does not exist".format(str(id)))
-
-        return music_object["title"]
-
- -
- -

Retrieves a music title given an ID

- -
Parameters
- -
    -
  • id: The music id to get the title for
  • -
-
- - -
-
-
#   - - - def - get_secuid(self, username, **kwargs): -
- -
- View Source -
    def get_secuid(self, username, **kwargs):
-        """Gets the secUid for a specific username
-
-        ##### Parameters
-        * username: The username to get the secUid for
-        """
-        r = requests.get(
-            "https://tiktok.com/@{}?lang=en".format(quote(username)),
-            headers={
-                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
-                "path": "/@{}".format(quote(username)),
-                "Accept-Encoding": "gzip, deflate",
-                "Connection": "keep-alive",
-                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36",
-            },
-            proxies=self.__format_proxy(
-                kwargs.get("proxy", None), cookies=self.get_cookies(**kwargs)
-            ),
-            **self.requests_extra_kwargs,
-        )
-        try:
-            return r.text.split('"secUid":"')[1].split('","secret":')[0]
-        except IndexError as e:
-            logging.info(r.text)
-            logging.error(e)
-            raise Exception(
-                "Retrieving the user secUid failed. Likely due to TikTok wanting captcha validation. Try to use a proxy."
-            )
-
- -
- -

Gets the secUid for a specific username

- -
Parameters
- -
    -
  • username: The username to get the secUid for
  • -
-
- - -
-
-
#   - -
@staticmethod
- - def - generate_device_id(): -
- -
- View Source -
    @staticmethod
-    def generate_device_id():
-        """Generates a valid device_id for other methods. Pass this as the custom_device_id field to download videos"""
-        return "".join([random.choice(string.digits) for num in range(19)])
-
- -
- -

Generates a valid device_id for other methods. Pass this as the custom_device_id field to download videos

-
- - -
-
-
- - - - \ No newline at end of file diff --git a/docs/TikTokApi/tiktokuser.html b/docs/TikTokApi/tiktokuser.html deleted file mode 100644 index 7266761d..00000000 --- a/docs/TikTokApi/tiktokuser.html +++ /dev/null @@ -1,590 +0,0 @@ - - - - - - - TikTokApi.tiktokuser API documentation - - - - - - - - -
-
-

-TikTokApi.tiktokuser

- - -
- View Source -
import requests
-
-
-class TikTokUser:
-    def __init__(self, user_cookie, debug=False, proxy=None):
-        """A TikTok User Class. Represents a single user that is logged in.
-
-        :param user_cookie: The cookies from a signed in session of TikTok.
-         Sign into TikTok.com and run document.cookie in the javascript console
-        and then copy the string and place it into this parameter.
-        """
-        self.cookies = user_cookie
-        self.debug = debug
-        self.proxy = proxy
-
-    def get_insights(self, videoID, username=None, proxy=None) -> dict:
-        """Get insights/analytics for a video.
-
-        :param videoID: The TikTok ID to look up the insights for.
-        """
-        api_url = "https://api.tiktok.com/aweme/v1/data/insighs/?tz_offset=-25200&aid=1233&carrier_region=US"
-        if username is not None:
-            referrer = "https://www.tiktok.com/@" + username + "/video/" + videoID
-        else:
-            referrer = "https://www.tiktok.com/"
-        insights = [
-            "video_info",
-            "video_page_percent",
-            "video_region_percent",
-            "video_total_duration",
-            "video_per_duration",
-        ]
-        # Note: this list of parameters has to be in exactly this order with exactly this format
-        # or else you will get "Invalid parameters"
-
-        def build_insight(insight, videoID):
-            return '{"insigh_type":"' + insight + '","aweme_id":"' + videoID + '"}'
-
-        insight_string = ",".join([build_insight(i, videoID) for i in insights])
-        insight_string = (
-            insight_string
-            + ',{"insigh_type": "user_info"}'
-            + ',{"insigh_type":"video_uv","aweme_id":"'
-            + videoID
-            + '"}'
-            + ',{"insigh_type":"vv_history","days":8}'
-            + ',{"insigh_type":"follower_num_history","days":9}'
-            + ',{"insigh_type":"follower_num"}'
-            + ',{"insigh_type":"user_info"}'
-        )
-        r = requests.post(
-            api_url,
-            headers={
-                "accept": "*/*",
-                "accept-language": "en-US,en;q=0.9",
-                "content-type": "application/x-www-form-urlencoded",
-                "sec-fetch-dest": "empty",
-                "sec-fetch-mode": "cors",
-                "sec-fetch-site": "same-site",
-                "referrer": referrer,
-                "referrerPolicy": "no-referrer-when-downgrade",
-                "method": "POST",
-                "mode": "cors",
-                "credentials": "include",
-            },
-            data="type_requests=[" + insight_string + "]",
-            proxies=self.__format_proxy(proxy),
-            cookies=self.__cookies_to_json(self.cookies),
-        )
-        try:
-            return r.json()
-        except Exception:
-            if debug:
-                print(f"Failed converting following to JSON\n{r.text}")
-            raise Exception("Invalid Response (from TikTok)")
-
-    #
-    # PRIVATE METHODS
-    #
-    def __format_proxy(self, proxy) -> dict:
-        """
-        Formats the proxy object
-        """
-        if proxy is not None:
-            return {"http": proxy, "https": proxy}
-        else:
-            return None
-
-    def __cookies_to_json(self, cookie_string) -> dict:
-        """
-        Turns a cookie string into a dict for
-        use in the requests module
-        """
-        if isinstance(cookie_string, dict):
-            return cookie_string
-
-        cookie_dict = {}
-        for cookie in cookie_string.split("; "):
-            cookie_dict[cookie.split("=")[0]] = cookie.split("=")[1]
-
-        return cookie_dict
-
- -
- -
-
-
- #   - - - class - TikTokUser: -
- -
- View Source -
class TikTokUser:
-    def __init__(self, user_cookie, debug=False, proxy=None):
-        """A TikTok User Class. Represents a single user that is logged in.
-
-        :param user_cookie: The cookies from a signed in session of TikTok.
-         Sign into TikTok.com and run document.cookie in the javascript console
-        and then copy the string and place it into this parameter.
-        """
-        self.cookies = user_cookie
-        self.debug = debug
-        self.proxy = proxy
-
-    def get_insights(self, videoID, username=None, proxy=None) -> dict:
-        """Get insights/analytics for a video.
-
-        :param videoID: The TikTok ID to look up the insights for.
-        """
-        api_url = "https://api.tiktok.com/aweme/v1/data/insighs/?tz_offset=-25200&aid=1233&carrier_region=US"
-        if username is not None:
-            referrer = "https://www.tiktok.com/@" + username + "/video/" + videoID
-        else:
-            referrer = "https://www.tiktok.com/"
-        insights = [
-            "video_info",
-            "video_page_percent",
-            "video_region_percent",
-            "video_total_duration",
-            "video_per_duration",
-        ]
-        # Note: this list of parameters has to be in exactly this order with exactly this format
-        # or else you will get "Invalid parameters"
-
-        def build_insight(insight, videoID):
-            return '{"insigh_type":"' + insight + '","aweme_id":"' + videoID + '"}'
-
-        insight_string = ",".join([build_insight(i, videoID) for i in insights])
-        insight_string = (
-            insight_string
-            + ',{"insigh_type": "user_info"}'
-            + ',{"insigh_type":"video_uv","aweme_id":"'
-            + videoID
-            + '"}'
-            + ',{"insigh_type":"vv_history","days":8}'
-            + ',{"insigh_type":"follower_num_history","days":9}'
-            + ',{"insigh_type":"follower_num"}'
-            + ',{"insigh_type":"user_info"}'
-        )
-        r = requests.post(
-            api_url,
-            headers={
-                "accept": "*/*",
-                "accept-language": "en-US,en;q=0.9",
-                "content-type": "application/x-www-form-urlencoded",
-                "sec-fetch-dest": "empty",
-                "sec-fetch-mode": "cors",
-                "sec-fetch-site": "same-site",
-                "referrer": referrer,
-                "referrerPolicy": "no-referrer-when-downgrade",
-                "method": "POST",
-                "mode": "cors",
-                "credentials": "include",
-            },
-            data="type_requests=[" + insight_string + "]",
-            proxies=self.__format_proxy(proxy),
-            cookies=self.__cookies_to_json(self.cookies),
-        )
-        try:
-            return r.json()
-        except Exception:
-            if debug:
-                print(f"Failed converting following to JSON\n{r.text}")
-            raise Exception("Invalid Response (from TikTok)")
-
-    #
-    # PRIVATE METHODS
-    #
-    def __format_proxy(self, proxy) -> dict:
-        """
-        Formats the proxy object
-        """
-        if proxy is not None:
-            return {"http": proxy, "https": proxy}
-        else:
-            return None
-
-    def __cookies_to_json(self, cookie_string) -> dict:
-        """
-        Turns a cookie string into a dict for
-        use in the requests module
-        """
-        if isinstance(cookie_string, dict):
-            return cookie_string
-
-        cookie_dict = {}
-        for cookie in cookie_string.split("; "):
-            cookie_dict[cookie.split("=")[0]] = cookie.split("=")[1]
-
-        return cookie_dict
-
- -
- - - -
-
#   - - - TikTokUser(user_cookie, debug=False, proxy=None) -
- -
- View Source -
    def __init__(self, user_cookie, debug=False, proxy=None):
-        """A TikTok User Class. Represents a single user that is logged in.
-
-        :param user_cookie: The cookies from a signed in session of TikTok.
-         Sign into TikTok.com and run document.cookie in the javascript console
-        and then copy the string and place it into this parameter.
-        """
-        self.cookies = user_cookie
-        self.debug = debug
-        self.proxy = proxy
-
- -
- -

A TikTok User Class. Represents a single user that is logged in.

- -

:param user_cookie: The cookies from a signed in session of TikTok. - Sign into TikTok.com and run document.cookie in the javascript console -and then copy the string and place it into this parameter.

-
- - -
-
-
#   - - - def - get_insights(self, videoID, username=None, proxy=None) -> dict: -
- -
- View Source -
    def get_insights(self, videoID, username=None, proxy=None) -> dict:
-        """Get insights/analytics for a video.
-
-        :param videoID: The TikTok ID to look up the insights for.
-        """
-        api_url = "https://api.tiktok.com/aweme/v1/data/insighs/?tz_offset=-25200&aid=1233&carrier_region=US"
-        if username is not None:
-            referrer = "https://www.tiktok.com/@" + username + "/video/" + videoID
-        else:
-            referrer = "https://www.tiktok.com/"
-        insights = [
-            "video_info",
-            "video_page_percent",
-            "video_region_percent",
-            "video_total_duration",
-            "video_per_duration",
-        ]
-        # Note: this list of parameters has to be in exactly this order with exactly this format
-        # or else you will get "Invalid parameters"
-
-        def build_insight(insight, videoID):
-            return '{"insigh_type":"' + insight + '","aweme_id":"' + videoID + '"}'
-
-        insight_string = ",".join([build_insight(i, videoID) for i in insights])
-        insight_string = (
-            insight_string
-            + ',{"insigh_type": "user_info"}'
-            + ',{"insigh_type":"video_uv","aweme_id":"'
-            + videoID
-            + '"}'
-            + ',{"insigh_type":"vv_history","days":8}'
-            + ',{"insigh_type":"follower_num_history","days":9}'
-            + ',{"insigh_type":"follower_num"}'
-            + ',{"insigh_type":"user_info"}'
-        )
-        r = requests.post(
-            api_url,
-            headers={
-                "accept": "*/*",
-                "accept-language": "en-US,en;q=0.9",
-                "content-type": "application/x-www-form-urlencoded",
-                "sec-fetch-dest": "empty",
-                "sec-fetch-mode": "cors",
-                "sec-fetch-site": "same-site",
-                "referrer": referrer,
-                "referrerPolicy": "no-referrer-when-downgrade",
-                "method": "POST",
-                "mode": "cors",
-                "credentials": "include",
-            },
-            data="type_requests=[" + insight_string + "]",
-            proxies=self.__format_proxy(proxy),
-            cookies=self.__cookies_to_json(self.cookies),
-        )
-        try:
-            return r.json()
-        except Exception:
-            if debug:
-                print(f"Failed converting following to JSON\n{r.text}")
-            raise Exception("Invalid Response (from TikTok)")
-
- -
- -

Get insights/analytics for a video.

- -

:param videoID: The TikTok ID to look up the insights for.

-
- - -
-
-
- - - - \ No newline at end of file diff --git a/docs/TikTokApi/utilities.html b/docs/TikTokApi/utilities.html deleted file mode 100644 index 1d75faef..00000000 --- a/docs/TikTokApi/utilities.html +++ /dev/null @@ -1,397 +0,0 @@ - - - - - - - TikTokApi.utilities API documentation - - - - - - - - -
-
-

-TikTokApi.utilities

- - -
- View Source -
import subprocess
-import sys
-import pkg_resources
-
-
-def update_messager():
-    if not check("TikTokApi"):
-        # Outdated
-        print(
-            "TikTokApi package is outdated, please consider upgrading! \n(You can suppress this by setting ignore_version=True in the TikTokApi constructor)"
-        )
-
-    if not check_future_deprecation():
-        print(
-            "Your version of python is going to be deprecated, for future updates upgrade to 3.7+"
-        )
-
-
-def check(name):
-
-    latest_version = str(
-        subprocess.run(
-            [sys.executable, "-m", "pip", "install", "{}==random".format(name)],
-            capture_output=True,
-            text=True,
-        )
-    )
-    latest_version = latest_version[latest_version.find("(from versions:") + 15 :]
-    latest_version = latest_version[: latest_version.find(")")]
-    latest_version = latest_version.replace(" ", "").split(",")[-1]
-
-    current_version = str(
-        subprocess.run(
-            [sys.executable, "-m", "pip", "show", "{}".format(name)],
-            capture_output=True,
-            text=True,
-        )
-    )
-    current_version = current_version[current_version.find("Version:") + 8 :]
-    current_version = current_version[: current_version.find("\\n")].replace(" ", "")
-
-    if latest_version == current_version:
-        return True
-    else:
-        return False
-
-
-def check_future_deprecation():
-    return sys.version_info >= (3, 7)
-
- -
- -
-
-
#   - - - def - update_messager(): -
- -
- View Source -
def update_messager():
-    if not check("TikTokApi"):
-        # Outdated
-        print(
-            "TikTokApi package is outdated, please consider upgrading! \n(You can suppress this by setting ignore_version=True in the TikTokApi constructor)"
-        )
-
-    if not check_future_deprecation():
-        print(
-            "Your version of python is going to be deprecated, for future updates upgrade to 3.7+"
-        )
-
- -
- - - -
-
-
#   - - - def - check(name): -
- -
- View Source -
def check(name):
-
-    latest_version = str(
-        subprocess.run(
-            [sys.executable, "-m", "pip", "install", "{}==random".format(name)],
-            capture_output=True,
-            text=True,
-        )
-    )
-    latest_version = latest_version[latest_version.find("(from versions:") + 15 :]
-    latest_version = latest_version[: latest_version.find(")")]
-    latest_version = latest_version.replace(" ", "").split(",")[-1]
-
-    current_version = str(
-        subprocess.run(
-            [sys.executable, "-m", "pip", "show", "{}".format(name)],
-            capture_output=True,
-            text=True,
-        )
-    )
-    current_version = current_version[current_version.find("Version:") + 8 :]
-    current_version = current_version[: current_version.find("\\n")].replace(" ", "")
-
-    if latest_version == current_version:
-        return True
-    else:
-        return False
-
- -
- - - -
-
-
#   - - - def - check_future_deprecation(): -
- -
- View Source -
def check_future_deprecation():
-    return sys.version_info >= (3, 7)
-
- -
- - - -
-
- - - - \ No newline at end of file diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index 6d90cc1e..00000000 --- a/docs/index.html +++ /dev/null @@ -1,259 +0,0 @@ - - - - - - - Module List – pdoc 7.2.0 - - - - - - - - - - -
- - pdoc logo - - -
-
-
- - - - \ No newline at end of file diff --git a/docs/search.json b/docs/search.json deleted file mode 100644 index 714c2b5a..00000000 --- a/docs/search.json +++ /dev/null @@ -1 +0,0 @@ -{"version": "0.9.5", "fields": ["qualname", "fullname", "doc"], "ref": "fullname", "documentStore": {"docs": {"TikTokApi": {"fullname": "TikTokApi", "modulename": "TikTokApi", "qualname": "", "type": "module", "doc": "

Unofficial TikTok API in Python

\n\n

This is an unofficial api wrapper for TikTok.com in python. With this api you are able to call most trending and fetch specific user information as well as much more.

\n\n

\"DOI\" \"LinkedIn\" \"Sponsor \"GitHub \"Build \"GitHub\" \"Downloads\" \"\" \"Support

\n\n

Sponsors

\n\n

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.

\n\n

\"TikAPI\" | TikAPI is a paid TikTok API service providing an full out-of-the-box solution for developers, trusted by 100+ companies. Learn more\n:-------------------------:|:-------------------------:

\n\n

Table of Contents

\n\n\n\n

Getting Started

\n\n

To get started using this api follow the instructions below.

\n\n

How to support the project

\n\n
    \n
  • Feel free to sponsor me on GitHub
  • \n
  • Feel free to tip the project using the brave browser
  • \n
  • Submit PRs for issues :)
  • \n
\n\n

Installing

\n\n

If you run into an issue please check the closed issues on the github. You're most likely not the first person to experience this issue. If nothing works feel free to open an issue.

\n\n
pip install TikTokApi\npython -m playwright install\n
\n\n

If you would prefer a video walk through of setting up this package I created a YouTube video just for that.

\n\n

If you're on MacOS you may need to install XCode Developer Tools

\n\n

Docker Installation

\n\n

Clone this repository onto a local machine then run the following commands.

\n\n
docker pull mcr.microsoft.com/playwright:focal\ndocker build . -t tiktokapi:latest\ndocker run -v TikTokApi --rm tiktokapi:latest python3 your_script.py\n
\n\n

Note this assumes your script is named your_script.py and lives in the root of this directory.

\n\n

Common Issues

\n\n

Please don't open an issue if you're experiencing one of these just comment if the provided solution do not work for you.

\n\n
    \n
  • Browser Has no Attribute - make sure you ran python3 -m playwright install, if your error persists try the playwright quickstart guide and diagnose issues from there.
  • \n
\n\n

Quick Start Guide

\n\n

Here's a quick bit of code to get the most recent trending on TikTok. There's more examples in the examples directory.

\n\n
from TikTokApi import TikTokApi\napi = TikTokApi.get_instance()\nresults = 10\n\n# Since TikTok changed their API you need to use the custom_verifyFp option. \n# In your web browser you will need to go to TikTok, Log in and get the s_v_web_id value.\ntrending = api.by_trending(count=results, custom_verifyFp="")\n\nfor tiktok in trending:\n    # Prints the id of the tiktok\n    print(tiktok['id'])\n\nprint(len(trending))\n
\n\n

To run the example scripts from the repository root, make sure you use the\nmodule form of python the interpreter

\n\n
python -m examples.get_trending\n
\n\n

Here's an example of what a TikTok dictionary looks like.

\n\n

Documentation

\n\n

You can find the documentation here (you'll likely just need the TikTokApi section of the docs), I will be making this documentation more complete overtime as it's not super great right now, but better than just having it in the readme!

\n\n

Authors

\n\n\n\n

See also the list of contributors who participated in this project.

\n\n

License

\n\n

This project is licensed under the MIT License

\n"}, "TikTokApi.browser_utilities": {"fullname": "TikTokApi.browser_utilities", "modulename": "TikTokApi.browser_utilities", "qualname": "", "type": "module", "doc": "

\n"}, "TikTokApi.browser_utilities.browser": {"fullname": "TikTokApi.browser_utilities.browser", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "", "type": "module", "doc": "

\n"}, "TikTokApi.browser_utilities.browser.get_playwright": {"fullname": "TikTokApi.browser_utilities.browser.get_playwright", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "get_playwright", "type": "function", "doc": "

\n", "parameters": [], "funcdef": "def"}, "TikTokApi.browser_utilities.browser.browser": {"fullname": "TikTokApi.browser_utilities.browser.browser", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser", "type": "class", "doc": "

Helper class that provides a standard way to create an ABC using\ninheritance.

\n"}, "TikTokApi.browser_utilities.browser.browser.__init__": {"fullname": "TikTokApi.browser_utilities.browser.browser.__init__", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.__init__", "type": "function", "doc": "

\n", "parameters": ["self", "kwargs"], "funcdef": "def"}, "TikTokApi.browser_utilities.browser.browser.get_params": {"fullname": "TikTokApi.browser_utilities.browser.browser.get_params", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.get_params", "type": "function", "doc": "

\n", "parameters": ["self", "page"], "funcdef": "def"}, "TikTokApi.browser_utilities.browser.browser.create_context": {"fullname": "TikTokApi.browser_utilities.browser.browser.create_context", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.create_context", "type": "function", "doc": "

\n", "parameters": ["self", "set_useragent"], "funcdef": "def"}, "TikTokApi.browser_utilities.browser.browser.base36encode": {"fullname": "TikTokApi.browser_utilities.browser.browser.base36encode", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.base36encode", "type": "function", "doc": "

Converts an integer to a base36 string.

\n", "parameters": ["self", "number", "alphabet"], "funcdef": "def"}, "TikTokApi.browser_utilities.browser.browser.gen_verifyFp": {"fullname": "TikTokApi.browser_utilities.browser.browser.gen_verifyFp", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.gen_verifyFp", "type": "function", "doc": "

\n", "parameters": ["self"], "funcdef": "def"}, "TikTokApi.browser_utilities.browser.browser.sign_url": {"fullname": "TikTokApi.browser_utilities.browser.browser.sign_url", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.sign_url", "type": "function", "doc": "

\n", "parameters": ["self", "calc_tt_params", "kwargs"], "funcdef": "def"}, "TikTokApi.browser_utilities.browser.browser.clean_up": {"fullname": "TikTokApi.browser_utilities.browser.browser.clean_up", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.clean_up", "type": "function", "doc": "

\n", "parameters": ["self"], "funcdef": "def"}, "TikTokApi.browser_utilities.browser.browser.find_redirect": {"fullname": "TikTokApi.browser_utilities.browser.browser.find_redirect", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.find_redirect", "type": "function", "doc": "

\n", "parameters": ["self", "url"], "funcdef": "def"}, "TikTokApi.browser_utilities.browser_interface": {"fullname": "TikTokApi.browser_utilities.browser_interface", "modulename": "TikTokApi.browser_utilities.browser_interface", "qualname": "", "type": "module", "doc": "

\n"}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface": {"fullname": "TikTokApi.browser_utilities.browser_interface.BrowserInterface", "modulename": "TikTokApi.browser_utilities.browser_interface", "qualname": "BrowserInterface", "type": "class", "doc": "

Helper class that provides a standard way to create an ABC using\ninheritance.

\n"}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.get_params": {"fullname": "TikTokApi.browser_utilities.browser_interface.BrowserInterface.get_params", "modulename": "TikTokApi.browser_utilities.browser_interface", "qualname": "BrowserInterface.get_params", "type": "function", "doc": "

\n", "parameters": ["self", "page"], "funcdef": "def"}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.sign_url": {"fullname": "TikTokApi.browser_utilities.browser_interface.BrowserInterface.sign_url", "modulename": "TikTokApi.browser_utilities.browser_interface", "qualname": "BrowserInterface.sign_url", "type": "function", "doc": "

\n", "parameters": ["self", "calc_tt_params", "kwargs"], "funcdef": "def"}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.clean_up": {"fullname": "TikTokApi.browser_utilities.browser_interface.BrowserInterface.clean_up", "modulename": "TikTokApi.browser_utilities.browser_interface", "qualname": "BrowserInterface.clean_up", "type": "function", "doc": "

\n", "parameters": ["self"], "funcdef": "def"}, "TikTokApi.browser_utilities.browser_selenium": {"fullname": "TikTokApi.browser_utilities.browser_selenium", "modulename": "TikTokApi.browser_utilities.browser_selenium", "qualname": "", "type": "module", "doc": "

\n"}, "TikTokApi.browser_utilities.browser_selenium.browser": {"fullname": "TikTokApi.browser_utilities.browser_selenium.browser", "modulename": "TikTokApi.browser_utilities.browser_selenium", "qualname": "browser", "type": "class", "doc": "

Helper class that provides a standard way to create an ABC using\ninheritance.

\n"}, "TikTokApi.browser_utilities.browser_selenium.browser.__init__": {"fullname": "TikTokApi.browser_utilities.browser_selenium.browser.__init__", "modulename": "TikTokApi.browser_utilities.browser_selenium", "qualname": "browser.__init__", "type": "function", "doc": "

\n", "parameters": ["self", "kwargs"], "funcdef": "def"}, "TikTokApi.browser_utilities.browser_selenium.browser.setup_browser": {"fullname": "TikTokApi.browser_utilities.browser_selenium.browser.setup_browser", "modulename": "TikTokApi.browser_utilities.browser_selenium", "qualname": "browser.setup_browser", "type": "function", "doc": "

\n", "parameters": ["self"], "funcdef": "def"}, "TikTokApi.browser_utilities.browser_selenium.browser.get_params": {"fullname": "TikTokApi.browser_utilities.browser_selenium.browser.get_params", "modulename": "TikTokApi.browser_utilities.browser_selenium", "qualname": "browser.get_params", "type": "function", "doc": "

\n", "parameters": ["self", "page"], "funcdef": "def"}, "TikTokApi.browser_utilities.browser_selenium.browser.base36encode": {"fullname": "TikTokApi.browser_utilities.browser_selenium.browser.base36encode", "modulename": "TikTokApi.browser_utilities.browser_selenium", "qualname": "browser.base36encode", "type": "function", "doc": "

Converts an integer to a base36 string.

\n", "parameters": ["self", "number", "alphabet"], "funcdef": "def"}, "TikTokApi.browser_utilities.browser_selenium.browser.gen_verifyFp": {"fullname": "TikTokApi.browser_utilities.browser_selenium.browser.gen_verifyFp", "modulename": "TikTokApi.browser_utilities.browser_selenium", "qualname": "browser.gen_verifyFp", "type": "function", "doc": "

\n", "parameters": ["self"], "funcdef": "def"}, "TikTokApi.browser_utilities.browser_selenium.browser.sign_url": {"fullname": "TikTokApi.browser_utilities.browser_selenium.browser.sign_url", "modulename": "TikTokApi.browser_utilities.browser_selenium", "qualname": "browser.sign_url", "type": "function", "doc": "

\n", "parameters": ["self", "calc_tt_params", "kwargs"], "funcdef": "def"}, "TikTokApi.browser_utilities.browser_selenium.browser.clean_up": {"fullname": "TikTokApi.browser_utilities.browser_selenium.browser.clean_up", "modulename": "TikTokApi.browser_utilities.browser_selenium", "qualname": "browser.clean_up", "type": "function", "doc": "

\n", "parameters": ["self"], "funcdef": "def"}, "TikTokApi.browser_utilities.get_acrawler": {"fullname": "TikTokApi.browser_utilities.get_acrawler", "modulename": "TikTokApi.browser_utilities.get_acrawler", "qualname": "", "type": "module", "doc": "

\n"}, "TikTokApi.browser_utilities.get_acrawler.get_tt_params_script": {"fullname": "TikTokApi.browser_utilities.get_acrawler.get_tt_params_script", "modulename": "TikTokApi.browser_utilities.get_acrawler", "qualname": "get_tt_params_script", "type": "function", "doc": "

\n", "parameters": [], "funcdef": "def"}, "TikTokApi.browser_utilities.get_acrawler.get_acrawler": {"fullname": "TikTokApi.browser_utilities.get_acrawler.get_acrawler", "modulename": "TikTokApi.browser_utilities.get_acrawler", "qualname": "get_acrawler", "type": "function", "doc": "

\n", "parameters": [], "funcdef": "def"}, "TikTokApi.browser_utilities.stealth": {"fullname": "TikTokApi.browser_utilities.stealth", "modulename": "TikTokApi.browser_utilities.stealth", "qualname": "", "type": "module", "doc": "

\n"}, "TikTokApi.browser_utilities.stealth.chrome_runtime": {"fullname": "TikTokApi.browser_utilities.stealth.chrome_runtime", "modulename": "TikTokApi.browser_utilities.stealth", "qualname": "chrome_runtime", "type": "function", "doc": "

\n", "parameters": ["page"], "funcdef": "def"}, "TikTokApi.browser_utilities.stealth.console_debug": {"fullname": "TikTokApi.browser_utilities.stealth.console_debug", "modulename": "TikTokApi.browser_utilities.stealth", "qualname": "console_debug", "type": "function", "doc": "

\n", "parameters": ["page"], "funcdef": "def"}, "TikTokApi.browser_utilities.stealth.iframe_content_window": {"fullname": "TikTokApi.browser_utilities.stealth.iframe_content_window", "modulename": "TikTokApi.browser_utilities.stealth", "qualname": "iframe_content_window", "type": "function", "doc": "

\n", "parameters": ["page"], "funcdef": "def"}, "TikTokApi.browser_utilities.stealth.media_codecs": {"fullname": "TikTokApi.browser_utilities.stealth.media_codecs", "modulename": "TikTokApi.browser_utilities.stealth", "qualname": "media_codecs", "type": "function", "doc": "

\n", "parameters": ["page"], "funcdef": "def"}, "TikTokApi.browser_utilities.stealth.navigator_languages": {"fullname": "TikTokApi.browser_utilities.stealth.navigator_languages", "modulename": "TikTokApi.browser_utilities.stealth", "qualname": "navigator_languages", "type": "function", "doc": "

\n", "parameters": ["page"], "funcdef": "def"}, "TikTokApi.browser_utilities.stealth.navigator_permissions": {"fullname": "TikTokApi.browser_utilities.stealth.navigator_permissions", "modulename": "TikTokApi.browser_utilities.stealth", "qualname": "navigator_permissions", "type": "function", "doc": "

\n", "parameters": ["page"], "funcdef": "def"}, "TikTokApi.browser_utilities.stealth.navigator_plugins": {"fullname": "TikTokApi.browser_utilities.stealth.navigator_plugins", "modulename": "TikTokApi.browser_utilities.stealth", "qualname": "navigator_plugins", "type": "function", "doc": "

\n", "parameters": ["page"], "funcdef": "def"}, "TikTokApi.browser_utilities.stealth.navigator_webdriver": {"fullname": "TikTokApi.browser_utilities.stealth.navigator_webdriver", "modulename": "TikTokApi.browser_utilities.stealth", "qualname": "navigator_webdriver", "type": "function", "doc": "

\n", "parameters": ["page"], "funcdef": "def"}, "TikTokApi.browser_utilities.stealth.user_agent": {"fullname": "TikTokApi.browser_utilities.stealth.user_agent", "modulename": "TikTokApi.browser_utilities.stealth", "qualname": "user_agent", "type": "function", "doc": "

\n", "parameters": ["page"], "funcdef": "def"}, "TikTokApi.browser_utilities.stealth.webgl_vendor": {"fullname": "TikTokApi.browser_utilities.stealth.webgl_vendor", "modulename": "TikTokApi.browser_utilities.stealth", "qualname": "webgl_vendor", "type": "function", "doc": "

\n", "parameters": ["page"], "funcdef": "def"}, "TikTokApi.browser_utilities.stealth.window_outerdimensions": {"fullname": "TikTokApi.browser_utilities.stealth.window_outerdimensions", "modulename": "TikTokApi.browser_utilities.stealth", "qualname": "window_outerdimensions", "type": "function", "doc": "

\n", "parameters": ["page"], "funcdef": "def"}, "TikTokApi.browser_utilities.stealth.stealth": {"fullname": "TikTokApi.browser_utilities.stealth.stealth", "modulename": "TikTokApi.browser_utilities.stealth", "qualname": "stealth", "type": "function", "doc": "

\n", "parameters": ["page"], "funcdef": "def"}, "TikTokApi.exceptions": {"fullname": "TikTokApi.exceptions", "modulename": "TikTokApi.exceptions", "qualname": "", "type": "module", "doc": "

\n"}, "TikTokApi.exceptions.TikTokCaptchaError": {"fullname": "TikTokApi.exceptions.TikTokCaptchaError", "modulename": "TikTokApi.exceptions", "qualname": "TikTokCaptchaError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n"}, "TikTokApi.exceptions.TikTokCaptchaError.__init__": {"fullname": "TikTokApi.exceptions.TikTokCaptchaError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "TikTokCaptchaError.__init__", "type": "function", "doc": "

\n", "parameters": ["self", "message"], "funcdef": "def"}, "TikTokApi.exceptions.GenericTikTokError": {"fullname": "TikTokApi.exceptions.GenericTikTokError", "modulename": "TikTokApi.exceptions", "qualname": "GenericTikTokError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n"}, "TikTokApi.exceptions.GenericTikTokError.__init__": {"fullname": "TikTokApi.exceptions.GenericTikTokError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "GenericTikTokError.__init__", "type": "function", "doc": "

\n", "parameters": ["self", "message"], "funcdef": "def"}, "TikTokApi.exceptions.TikTokNotFoundError": {"fullname": "TikTokApi.exceptions.TikTokNotFoundError", "modulename": "TikTokApi.exceptions", "qualname": "TikTokNotFoundError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n"}, "TikTokApi.exceptions.TikTokNotFoundError.__init__": {"fullname": "TikTokApi.exceptions.TikTokNotFoundError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "TikTokNotFoundError.__init__", "type": "function", "doc": "

\n", "parameters": ["self", "message"], "funcdef": "def"}, "TikTokApi.exceptions.EmptyResponseError": {"fullname": "TikTokApi.exceptions.EmptyResponseError", "modulename": "TikTokApi.exceptions", "qualname": "EmptyResponseError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n"}, "TikTokApi.exceptions.EmptyResponseError.__init__": {"fullname": "TikTokApi.exceptions.EmptyResponseError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "EmptyResponseError.__init__", "type": "function", "doc": "

\n", "parameters": ["self", "message"], "funcdef": "def"}, "TikTokApi.exceptions.JSONDecodeFailure": {"fullname": "TikTokApi.exceptions.JSONDecodeFailure", "modulename": "TikTokApi.exceptions", "qualname": "JSONDecodeFailure", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n"}, "TikTokApi.exceptions.JSONDecodeFailure.__init__": {"fullname": "TikTokApi.exceptions.JSONDecodeFailure.__init__", "modulename": "TikTokApi.exceptions", "qualname": "JSONDecodeFailure.__init__", "type": "function", "doc": "

\n", "parameters": ["self", "message"], "funcdef": "def"}, "TikTokApi.exceptions.TikTokNotAvailableError": {"fullname": "TikTokApi.exceptions.TikTokNotAvailableError", "modulename": "TikTokApi.exceptions", "qualname": "TikTokNotAvailableError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n"}, "TikTokApi.exceptions.TikTokNotAvailableError.__init__": {"fullname": "TikTokApi.exceptions.TikTokNotAvailableError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "TikTokNotAvailableError.__init__", "type": "function", "doc": "

\n", "parameters": ["self", "message"], "funcdef": "def"}, "TikTokApi.tiktok": {"fullname": "TikTokApi.tiktok", "modulename": "TikTokApi.tiktok", "qualname": "", "type": "module", "doc": "

\n"}, "TikTokApi.tiktok.TikTokApi": {"fullname": "TikTokApi.tiktok.TikTokApi", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi", "type": "class", "doc": "

\n"}, "TikTokApi.tiktok.TikTokApi.__init__": {"fullname": "TikTokApi.tiktok.TikTokApi.__init__", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.__init__", "type": "function", "doc": "

The TikTokApi class. Used to interact with TikTok, use get_instance NOT this.

\n", "parameters": ["self", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_instance": {"fullname": "TikTokApi.tiktok.TikTokApi.get_instance", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_instance", "type": "function", "doc": "

The TikTokApi class. Used to interact with TikTok. This is a singleton\n class to prevent issues from arising with playwright

\n\n
Parameters
\n\n
    \n
  • logging_level: The logging level you want the program to run at, optional\nThese are the standard python logging module's levels.

  • \n
  • request_delay: The amount of time in seconds to wait before making a request, optional\nThis is used to throttle your own requests as you may end up making too\nmany requests to TikTok for your IP.

  • \n
  • custom_device_id: A TikTok parameter needed to download videos, optional\nThe code generates these and handles these pretty well itself, however\nfor some things such as video download you will need to set a consistent\none of these. All the methods take this as a optional parameter, however\nit's cleaner code to store this at the instance level. You can override\nthis at the specific methods.

  • \n
  • generate_static_device_id: A parameter that generates a custom_device_id at the instance level\nUse this if you want to download videos from a script but don't want to generate\nyour own custom_device_id parameter.

  • \n
  • custom_verifyFp: A TikTok parameter needed to work most of the time, optional\nTo get this parameter look at this video\nI recommend watching the entire thing, as it will help setup this package. All\nthe methods take this as a optional parameter, however it's cleaner code\nto store this at the instance level. You can override this at the specific\nmethods.

    \n\n

    You can use the following to generate \"\".join(random.choice(string.digits)\nfor num in range(19))

  • \n
  • use_test_endpoints: Send requests to TikTok's test endpoints, optional\nThis parameter when set to true will make requests to TikTok's testing\nendpoints instead of the live site. I can't guarantee this will work\nin the future, however currently basically any custom_verifyFp will\nwork here which is helpful.

  • \n
  • proxy: A string containing your proxy address, optional\nIf you want to do a lot of scraping of TikTok endpoints you'll likely\nneed a proxy.

    \n\n

    Ex: \"https://0.0.0.0:8080\"

    \n\n

    All the methods take this as a optional parameter, however it's cleaner code\nto store this at the instance level. You can override this at the specific\nmethods.

  • \n
  • use_selenium: Option to use selenium over playwright, optional\nPlaywright is selected by default and is the one that I'm designing the\npackage to be compatable for, however if playwright doesn't work on\nyour machine feel free to set this to True.

  • \n
  • executablePath: The location of the driver, optional\nThis shouldn't be needed if you're using playwright

  • \n
  • **kwargs\nParameters that are passed on to basically every module and methods\nthat interact with this main class. These may or may not be documented\nin other places.

  • \n
\n", "parameters": ["kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.clean_up": {"fullname": "TikTokApi.tiktok.TikTokApi.clean_up", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.clean_up", "type": "function", "doc": "

A basic cleanup method, called automatically from the code

\n", "parameters": ["self"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.external_signer": {"fullname": "TikTokApi.tiktok.TikTokApi.external_signer", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.external_signer", "type": "function", "doc": "

Makes requests to an external signer instead of using a browser.

\n\n
Parameters
\n\n
    \n
  • url: The server to make requests to\nThis server is designed to sign requests. You can find an example\nof this signature server in the examples folder.

  • \n
  • custom_device_id: A TikTok parameter needed to download videos\nThe code generates these and handles these pretty well itself, however\nfor some things such as video download you will need to set a consistent\none of these.

  • \n
  • custom_verifyFp: A TikTok parameter needed to work most of the time,\nTo get this parameter look at this video\nI recommend watching the entire thing, as it will help setup this package.

  • \n
\n", "parameters": ["self", "url", "custom_device_id", "verifyFp"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_data": {"fullname": "TikTokApi.tiktok.TikTokApi.get_data", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_data", "type": "function", "doc": "

Makes requests to TikTok and returns their JSON.

\n\n

This is all handled by the package so it's unlikely\nyou will need to use this.

\n", "parameters": ["self", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_cookies": {"fullname": "TikTokApi.tiktok.TikTokApi.get_cookies", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_cookies", "type": "function", "doc": "

Extracts cookies from the kwargs passed to the function for get_data

\n", "parameters": ["self", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_bytes": {"fullname": "TikTokApi.tiktok.TikTokApi.get_bytes", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_bytes", "type": "function", "doc": "

Returns TikTok's response as bytes, similar to get_data

\n", "parameters": ["self", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.by_trending": {"fullname": "TikTokApi.tiktok.TikTokApi.by_trending", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.by_trending", "type": "function", "doc": "

Gets trending TikToks

\n\n
Parameters
\n\n
    \n
  • count: The amount of TikToks you want returned, optional

    \n\n

    Note: TikTok seems to only support at MOST ~2000 TikToks\nfrom a single endpoint.

  • \n
\n", "parameters": ["self", "count", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.search_for_users": {"fullname": "TikTokApi.tiktok.TikTokApi.search_for_users", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.search_for_users", "type": "function", "doc": "

Returns a list of users that match the search_term

\n\n
Parameters
\n\n
    \n
  • search_term: The string to search for users by\nThis string is the term you want to search for users by.

  • \n
  • count: The number of users to return\nNote: maximum is around 28 for this type of endpoint.

  • \n
\n", "parameters": ["self", "search_term", "count", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"fullname": "TikTokApi.tiktok.TikTokApi.search_for_music", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.search_for_music", "type": "function", "doc": "

Returns a list of music that match the search_term

\n\n
Parameters
\n\n
    \n
  • search_term: The string to search for music by\nThis string is the term you want to search for music by.

  • \n
  • count: The number of music to return\nNote: maximum is around 28 for this type of endpoint.

  • \n
\n", "parameters": ["self", "search_term", "count", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"fullname": "TikTokApi.tiktok.TikTokApi.search_for_hashtags", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.search_for_hashtags", "type": "function", "doc": "

Returns a list of hashtags that match the search_term

\n\n
Parameters
\n\n
    \n
  • search_term: The string to search for music by\nThis string is the term you want to search for music by.

  • \n
  • count: The number of music to return\nNote: maximum is around 28 for this type of endpoint.

  • \n
\n", "parameters": ["self", "search_term", "count", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.discover_type": {"fullname": "TikTokApi.tiktok.TikTokApi.discover_type", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.discover_type", "type": "function", "doc": "

Returns a list of whatever the prefix type you pass in

\n\n
Parameters
\n\n
    \n
  • search_term: The string to search by

  • \n
  • prefix: The prefix of what to search for

  • \n
  • count: The number search results to return\nNote: maximum is around 28 for this type of endpoint.

  • \n
\n", "parameters": ["self", "search_term", "prefix", "count", "offset", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.user_posts": {"fullname": "TikTokApi.tiktok.TikTokApi.user_posts", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.user_posts", "type": "function", "doc": "

Returns an array of dictionaries representing TikToks for a user.

\n\n
Parameters
\n\n
    \n
  • userID: The userID of the user, which TikTok assigns

    \n\n

    You can find this from utilizing other methods or\njust use by_username to find it.

  • \n
  • secUID: The secUID of the user, which TikTok assigns

    \n\n

    You can find this from utilizing other methods or\njust use by_username to find it.

  • \n
  • count: The number of posts to return

    \n\n

    Note: seems to only support up to ~2,000

  • \n
\n", "parameters": ["self", "userID", "secUID", "count", "cursor", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.by_username": {"fullname": "TikTokApi.tiktok.TikTokApi.by_username", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.by_username", "type": "function", "doc": "

Returns a dictionary listing TikToks given a user's username.

\n\n
Parameters
\n\n
    \n
  • username: The username of the TikTok user to get TikToks from

  • \n
  • count: The number of posts to return

    \n\n

    Note: seems to only support up to ~2,000

  • \n
\n", "parameters": ["self", "username", "count", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.user_page": {"fullname": "TikTokApi.tiktok.TikTokApi.user_page", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.user_page", "type": "function", "doc": "

Returns a dictionary listing of one page of TikToks given a user's ID and secUID

\n\n
Parameters
\n\n
    \n
  • userID: The userID of the user, which TikTok assigns

    \n\n

    You can find this from utilizing other methods or\njust use by_username to find it.

  • \n
  • secUID: The secUID of the user, which TikTok assigns

    \n\n

    You can find this from utilizing other methods or\njust use by_username to find it.

  • \n
  • page_size: The number of posts to return per page

    \n\n

    Gets a specific page of a user, doesn't iterate.

  • \n
  • cursor: The offset of a page

    \n\n

    The offset to return new videos from

  • \n
\n", "parameters": ["self", "userID", "secUID", "page_size", "cursor", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"fullname": "TikTokApi.tiktok.TikTokApi.get_user_pager", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_user_pager", "type": "function", "doc": "

Returns a generator to page through a user's feed

\n\n
Parameters
\n\n
    \n
  • username: The username of the user

  • \n
  • page_size: The number of posts to return in a page

  • \n
  • cursor: The offset of a page

    \n\n

    The offset to return new videos from

  • \n
\n", "parameters": ["self", "username", "page_size", "cursor", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.user_liked": {"fullname": "TikTokApi.tiktok.TikTokApi.user_liked", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.user_liked", "type": "function", "doc": "

Returns a dictionary listing TikToks that a given a user has liked.\n Note: The user's likes must be public

\n\n
Parameters
\n\n
    \n
  • userID: The userID of the user, which TikTok assigns

  • \n
  • secUID: The secUID of the user, which TikTok assigns

  • \n
  • count: The number of posts to return

    \n\n

    Note: seems to only support up to ~2,000

  • \n
  • cursor: The offset of a page

    \n\n

    The offset to return new videos from

  • \n
\n", "parameters": ["self", "userID", "secUID", "count", "cursor", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"fullname": "TikTokApi.tiktok.TikTokApi.user_liked_by_username", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.user_liked_by_username", "type": "function", "doc": "

Returns a dictionary listing TikToks a user has liked by username.\n Note: The user's likes must be public

\n\n
Parameters
\n\n
    \n
  • username: The username of the user

  • \n
  • count: The number of posts to return

    \n\n

    Note: seems to only support up to ~2,000

  • \n
\n", "parameters": ["self", "username", "count", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.by_sound": {"fullname": "TikTokApi.tiktok.TikTokApi.by_sound", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.by_sound", "type": "function", "doc": "

Returns a dictionary listing TikToks with a specific sound.

\n\n
Parameters
\n\n
    \n
  • id: The sound id to search by

    \n\n

    Note: Can be found in the URL of the sound specific page or with other methods.

  • \n
  • count: The number of posts to return

    \n\n

    Note: seems to only support up to ~2,000

  • \n
\n", "parameters": ["self", "id", "count", "offset", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"fullname": "TikTokApi.tiktok.TikTokApi.by_sound_page", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.by_sound_page", "type": "function", "doc": "

Returns a page of tiktoks with a specific sound.

\n\n

Parameters

\n\n

id: The sound id to search by\n Note: Can be found in the URL of the sound specific page or with other methods.\ncursor: offset for pagination\npage_size: The number of posts to return

\n", "parameters": ["self", "id", "page_size", "cursor", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_music_object": {"fullname": "TikTokApi.tiktok.TikTokApi.get_music_object", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_music_object", "type": "function", "doc": "

Returns a music object for a specific sound id.

\n\n
Parameters
\n\n
    \n
  • id: The sound id to get the object for

    \n\n

    This can be found by using other methods.

  • \n
\n", "parameters": ["self", "id", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_music_object_full": {"fullname": "TikTokApi.tiktok.TikTokApi.get_music_object_full", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_music_object_full", "type": "function", "doc": "

Returns a music object for a specific sound id.

\n\n
Parameters
\n\n
    \n
  • id: The sound id to get the object for

    \n\n

    This can be found by using other methods.

  • \n
\n", "parameters": ["self", "id", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"fullname": "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_music_object_full_by_api", "type": "function", "doc": "

Returns a music object for a specific sound id, but using the API rather than HTML requests.

\n\n
Parameters
\n\n
    \n
  • id: The sound id to get the object for

    \n\n

    This can be found by using other methods.

  • \n
\n", "parameters": ["self", "id", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"fullname": "TikTokApi.tiktok.TikTokApi.by_hashtag", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.by_hashtag", "type": "function", "doc": "

Returns a dictionary listing TikToks with a specific hashtag.

\n\n
Parameters
\n\n
    \n
  • hashtag: The hashtag to search by

    \n\n

    Without the # symbol

    \n\n

    A valid string is \"funny\"

  • \n
  • count: The number of posts to return\nNote: seems to only support up to ~2,000
  • \n
\n", "parameters": ["self", "hashtag", "count", "offset", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_hashtag_object": {"fullname": "TikTokApi.tiktok.TikTokApi.get_hashtag_object", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_hashtag_object", "type": "function", "doc": "

Returns a hashtag object.

\n\n
Parameters
\n\n
    \n
  • hashtag: The hashtag to search by

    \n\n

    Without the # symbol

  • \n
\n", "parameters": ["self", "hashtag", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"fullname": "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_recommended_tiktoks_by_video_id", "type": "function", "doc": "

Returns a dictionary listing reccomended TikToks for a specific TikTok video.

\n\n
Parameters
\n\n
    \n
  • id: The id of the video to get suggestions for

    \n\n

    Can be found using other methods

  • \n
  • count: The count of results you want to return
  • \n
\n", "parameters": ["self", "id", "count", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_id": {"fullname": "TikTokApi.tiktok.TikTokApi.get_tiktok_by_id", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_tiktok_by_id", "type": "function", "doc": "

Returns a dictionary of a specific TikTok.

\n\n
Parameters
\n\n
    \n
  • id: The id of the TikTok you want to get the object for
  • \n
\n", "parameters": ["self", "id", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_url": {"fullname": "TikTokApi.tiktok.TikTokApi.get_tiktok_by_url", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_tiktok_by_url", "type": "function", "doc": "

Returns a dictionary of a TikTok object by url.

\n\n
Parameters
\n\n
    \n
  • url: The TikTok url you want to retrieve
  • \n
\n", "parameters": ["self", "url", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"fullname": "TikTokApi.tiktok.TikTokApi.get_tiktok_by_html", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_tiktok_by_html", "type": "function", "doc": "

This method retrieves a TikTok using the html\nendpoints rather than the API based ones.

\n\n
Parameters
\n\n
    \n
  • url: The url of the TikTok to get
  • \n
\n", "parameters": ["self", "url", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_user_object": {"fullname": "TikTokApi.tiktok.TikTokApi.get_user_object", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_user_object", "type": "function", "doc": "

Gets a user object (dictionary)

\n\n
Parameters
\n\n
    \n
  • username: The username of the user
  • \n
\n", "parameters": ["self", "username", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_user": {"fullname": "TikTokApi.tiktok.TikTokApi.get_user", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_user", "type": "function", "doc": "

Gets the full exposed user object

\n\n
Parameters
\n\n
    \n
  • username: The username of the user
  • \n
\n", "parameters": ["self", "username", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"fullname": "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_video_by_tiktok", "type": "function", "doc": "

Downloads video from TikTok using a TikTok object.

\n\n

You will need to set a custom_device_id to do this for anything but trending.\nTo do this, this is pretty simple you can either generate one yourself or,\nyou can pass the generate_static_device_id=True into the constructor of the\nTikTokApi class.

\n\n
Parameters
\n\n
    \n
  • data: A TikTok object

    \n\n

    A TikTok JSON object from any other method.

  • \n
\n", "parameters": ["self", "data", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_video_by_download_url": {"fullname": "TikTokApi.tiktok.TikTokApi.get_video_by_download_url", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_video_by_download_url", "type": "function", "doc": "

Downloads video from TikTok using download url in a TikTok object

\n\n
Parameters
\n\n
    \n
  • download_url: The download url key value in a TikTok object
  • \n
\n", "parameters": ["self", "download_url", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_video_by_url": {"fullname": "TikTokApi.tiktok.TikTokApi.get_video_by_url", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_video_by_url", "type": "function", "doc": "

Downloads a TikTok video by a URL

\n\n
Parameters
\n\n
    \n
  • video_url: The TikTok url to download the video from
  • \n
\n", "parameters": ["self", "video_url", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"fullname": "TikTokApi.tiktok.TikTokApi.get_video_no_watermark", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_video_no_watermark", "type": "function", "doc": "

Gets the video with no watermark\n.. deprecated::

\n\n

Deprecated due to TikTok fixing this

\n\n
Parameters
\n\n
    \n
  • video_url: The url of the video you want to download

  • \n
  • return_bytes: Set this to 0 if you want url, 1 if you want bytes

  • \n
\n", "parameters": ["self", "video_url", "return_bytes", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_music_title": {"fullname": "TikTokApi.tiktok.TikTokApi.get_music_title", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_music_title", "type": "function", "doc": "

Retrieves a music title given an ID

\n\n
Parameters
\n\n
    \n
  • id: The music id to get the title for
  • \n
\n", "parameters": ["self", "id", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.get_secuid": {"fullname": "TikTokApi.tiktok.TikTokApi.get_secuid", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_secuid", "type": "function", "doc": "

Gets the secUid for a specific username

\n\n
Parameters
\n\n
    \n
  • username: The username to get the secUid for
  • \n
\n", "parameters": ["self", "username", "kwargs"], "funcdef": "def"}, "TikTokApi.tiktok.TikTokApi.generate_device_id": {"fullname": "TikTokApi.tiktok.TikTokApi.generate_device_id", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.generate_device_id", "type": "function", "doc": "

Generates a valid device_id for other methods. Pass this as the custom_device_id field to download videos

\n", "parameters": [], "funcdef": "def"}, "TikTokApi.tiktokuser": {"fullname": "TikTokApi.tiktokuser", "modulename": "TikTokApi.tiktokuser", "qualname": "", "type": "module", "doc": "

\n"}, "TikTokApi.tiktokuser.TikTokUser": {"fullname": "TikTokApi.tiktokuser.TikTokUser", "modulename": "TikTokApi.tiktokuser", "qualname": "TikTokUser", "type": "class", "doc": "

\n"}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"fullname": "TikTokApi.tiktokuser.TikTokUser.__init__", "modulename": "TikTokApi.tiktokuser", "qualname": "TikTokUser.__init__", "type": "function", "doc": "

A TikTok User Class. Represents a single user that is logged in.

\n\n

:param user_cookie: The cookies from a signed in session of TikTok.\n Sign into TikTok.com and run document.cookie in the javascript console\nand then copy the string and place it into this parameter.

\n", "parameters": ["self", "user_cookie", "debug", "proxy"], "funcdef": "def"}, "TikTokApi.tiktokuser.TikTokUser.get_insights": {"fullname": "TikTokApi.tiktokuser.TikTokUser.get_insights", "modulename": "TikTokApi.tiktokuser", "qualname": "TikTokUser.get_insights", "type": "function", "doc": "

Get insights/analytics for a video.

\n\n

:param videoID: The TikTok ID to look up the insights for.

\n", "parameters": ["self", "videoID", "username", "proxy"], "funcdef": "def"}, "TikTokApi.utilities": {"fullname": "TikTokApi.utilities", "modulename": "TikTokApi.utilities", "qualname": "", "type": "module", "doc": "

\n"}, "TikTokApi.utilities.update_messager": {"fullname": "TikTokApi.utilities.update_messager", "modulename": "TikTokApi.utilities", "qualname": "update_messager", "type": "function", "doc": "

\n", "parameters": [], "funcdef": "def"}, "TikTokApi.utilities.check": {"fullname": "TikTokApi.utilities.check", "modulename": "TikTokApi.utilities", "qualname": "check", "type": "function", "doc": "

\n", "parameters": ["name"], "funcdef": "def"}, "TikTokApi.utilities.check_future_deprecation": {"fullname": "TikTokApi.utilities.check_future_deprecation", "modulename": "TikTokApi.utilities", "qualname": "check_future_deprecation", "type": "function", "doc": "

\n", "parameters": [], "funcdef": "def"}}, "docInfo": {"TikTokApi": {"qualname": 0, "fullname": 1, "doc": 325}, "TikTokApi.browser_utilities": {"qualname": 0, "fullname": 2, "doc": 0}, "TikTokApi.browser_utilities.browser": {"qualname": 0, "fullname": 3, "doc": 0}, "TikTokApi.browser_utilities.browser.get_playwright": {"qualname": 1, "fullname": 4, "doc": 0}, "TikTokApi.browser_utilities.browser.browser": {"qualname": 1, "fullname": 4, "doc": 9}, "TikTokApi.browser_utilities.browser.browser.__init__": {"qualname": 2, "fullname": 5, "doc": 0}, "TikTokApi.browser_utilities.browser.browser.get_params": {"qualname": 2, "fullname": 5, "doc": 0}, "TikTokApi.browser_utilities.browser.browser.create_context": {"qualname": 2, "fullname": 5, "doc": 0}, "TikTokApi.browser_utilities.browser.browser.base36encode": {"qualname": 2, "fullname": 5, "doc": 4}, "TikTokApi.browser_utilities.browser.browser.gen_verifyFp": {"qualname": 2, "fullname": 5, "doc": 0}, "TikTokApi.browser_utilities.browser.browser.sign_url": {"qualname": 2, "fullname": 5, "doc": 0}, "TikTokApi.browser_utilities.browser.browser.clean_up": {"qualname": 2, "fullname": 5, "doc": 0}, "TikTokApi.browser_utilities.browser.browser.find_redirect": {"qualname": 2, "fullname": 5, "doc": 0}, "TikTokApi.browser_utilities.browser_interface": {"qualname": 0, "fullname": 3, "doc": 0}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface": {"qualname": 1, "fullname": 4, "doc": 9}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.get_params": {"qualname": 2, "fullname": 5, "doc": 0}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.sign_url": {"qualname": 2, "fullname": 5, "doc": 0}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.clean_up": {"qualname": 2, "fullname": 5, "doc": 0}, "TikTokApi.browser_utilities.browser_selenium": {"qualname": 0, "fullname": 3, "doc": 0}, "TikTokApi.browser_utilities.browser_selenium.browser": {"qualname": 1, "fullname": 4, "doc": 9}, "TikTokApi.browser_utilities.browser_selenium.browser.__init__": {"qualname": 2, "fullname": 5, "doc": 0}, "TikTokApi.browser_utilities.browser_selenium.browser.setup_browser": {"qualname": 2, "fullname": 5, "doc": 0}, "TikTokApi.browser_utilities.browser_selenium.browser.get_params": {"qualname": 2, "fullname": 5, "doc": 0}, "TikTokApi.browser_utilities.browser_selenium.browser.base36encode": {"qualname": 2, "fullname": 5, "doc": 4}, "TikTokApi.browser_utilities.browser_selenium.browser.gen_verifyFp": {"qualname": 2, "fullname": 5, "doc": 0}, "TikTokApi.browser_utilities.browser_selenium.browser.sign_url": {"qualname": 2, "fullname": 5, "doc": 0}, "TikTokApi.browser_utilities.browser_selenium.browser.clean_up": {"qualname": 2, "fullname": 5, "doc": 0}, "TikTokApi.browser_utilities.get_acrawler": {"qualname": 0, "fullname": 3, "doc": 0}, "TikTokApi.browser_utilities.get_acrawler.get_tt_params_script": {"qualname": 1, "fullname": 4, "doc": 0}, "TikTokApi.browser_utilities.get_acrawler.get_acrawler": {"qualname": 1, "fullname": 4, "doc": 0}, "TikTokApi.browser_utilities.stealth": {"qualname": 0, "fullname": 3, "doc": 0}, "TikTokApi.browser_utilities.stealth.chrome_runtime": {"qualname": 1, "fullname": 4, "doc": 0}, "TikTokApi.browser_utilities.stealth.console_debug": {"qualname": 1, "fullname": 4, "doc": 0}, "TikTokApi.browser_utilities.stealth.iframe_content_window": {"qualname": 1, "fullname": 4, "doc": 0}, "TikTokApi.browser_utilities.stealth.media_codecs": {"qualname": 1, "fullname": 4, "doc": 0}, "TikTokApi.browser_utilities.stealth.navigator_languages": {"qualname": 1, "fullname": 4, "doc": 0}, "TikTokApi.browser_utilities.stealth.navigator_permissions": {"qualname": 1, "fullname": 4, "doc": 0}, "TikTokApi.browser_utilities.stealth.navigator_plugins": {"qualname": 1, "fullname": 4, "doc": 0}, "TikTokApi.browser_utilities.stealth.navigator_webdriver": {"qualname": 1, "fullname": 4, "doc": 0}, "TikTokApi.browser_utilities.stealth.user_agent": {"qualname": 1, "fullname": 4, "doc": 0}, "TikTokApi.browser_utilities.stealth.webgl_vendor": {"qualname": 1, "fullname": 4, "doc": 0}, "TikTokApi.browser_utilities.stealth.window_outerdimensions": {"qualname": 1, "fullname": 4, "doc": 0}, "TikTokApi.browser_utilities.stealth.stealth": {"qualname": 1, "fullname": 4, "doc": 0}, "TikTokApi.exceptions": {"qualname": 0, "fullname": 2, "doc": 0}, "TikTokApi.exceptions.TikTokCaptchaError": {"qualname": 1, "fullname": 3, "doc": 6}, "TikTokApi.exceptions.TikTokCaptchaError.__init__": {"qualname": 2, "fullname": 4, "doc": 0}, "TikTokApi.exceptions.GenericTikTokError": {"qualname": 1, "fullname": 3, "doc": 6}, "TikTokApi.exceptions.GenericTikTokError.__init__": {"qualname": 2, "fullname": 4, "doc": 0}, "TikTokApi.exceptions.TikTokNotFoundError": {"qualname": 1, "fullname": 3, "doc": 6}, "TikTokApi.exceptions.TikTokNotFoundError.__init__": {"qualname": 2, "fullname": 4, "doc": 0}, "TikTokApi.exceptions.EmptyResponseError": {"qualname": 1, "fullname": 3, "doc": 6}, "TikTokApi.exceptions.EmptyResponseError.__init__": {"qualname": 2, "fullname": 4, "doc": 0}, "TikTokApi.exceptions.JSONDecodeFailure": {"qualname": 1, "fullname": 3, "doc": 6}, "TikTokApi.exceptions.JSONDecodeFailure.__init__": {"qualname": 2, "fullname": 4, "doc": 0}, "TikTokApi.exceptions.TikTokNotAvailableError": {"qualname": 1, "fullname": 3, "doc": 6}, "TikTokApi.exceptions.TikTokNotAvailableError.__init__": {"qualname": 2, "fullname": 4, "doc": 0}, "TikTokApi.tiktok": {"qualname": 0, "fullname": 2, "doc": 0}, "TikTokApi.tiktok.TikTokApi": {"qualname": 1, "fullname": 3, "doc": 0}, "TikTokApi.tiktok.TikTokApi.__init__": {"qualname": 2, "fullname": 4, "doc": 7}, "TikTokApi.tiktok.TikTokApi.get_instance": {"qualname": 2, "fullname": 4, "doc": 234}, "TikTokApi.tiktok.TikTokApi.clean_up": {"qualname": 2, "fullname": 4, "doc": 6}, "TikTokApi.tiktok.TikTokApi.external_signer": {"qualname": 2, "fullname": 4, "doc": 58}, "TikTokApi.tiktok.TikTokApi.get_data": {"qualname": 2, "fullname": 4, "doc": 11}, "TikTokApi.tiktok.TikTokApi.get_cookies": {"qualname": 2, "fullname": 4, "doc": 6}, "TikTokApi.tiktok.TikTokApi.get_bytes": {"qualname": 2, "fullname": 4, "doc": 6}, "TikTokApi.tiktok.TikTokApi.by_trending": {"qualname": 2, "fullname": 4, "doc": 18}, "TikTokApi.tiktok.TikTokApi.search_for_users": {"qualname": 2, "fullname": 4, "doc": 25}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"qualname": 2, "fullname": 4, "doc": 25}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"qualname": 2, "fullname": 4, "doc": 25}, "TikTokApi.tiktok.TikTokApi.discover_type": {"qualname": 2, "fullname": 4, "doc": 24}, "TikTokApi.tiktok.TikTokApi.user_posts": {"qualname": 2, "fullname": 4, "doc": 38}, "TikTokApi.tiktok.TikTokApi.by_username": {"qualname": 2, "fullname": 4, "doc": 22}, "TikTokApi.tiktok.TikTokApi.user_page": {"qualname": 2, "fullname": 4, "doc": 52}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"qualname": 2, "fullname": 4, "doc": 22}, "TikTokApi.tiktok.TikTokApi.user_liked": {"qualname": 2, "fullname": 4, "doc": 38}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"qualname": 2, "fullname": 4, "doc": 24}, "TikTokApi.tiktok.TikTokApi.by_sound": {"qualname": 2, "fullname": 4, "doc": 27}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"qualname": 2, "fullname": 4, "doc": 24}, "TikTokApi.tiktok.TikTokApi.get_music_object": {"qualname": 2, "fullname": 4, "doc": 14}, "TikTokApi.tiktok.TikTokApi.get_music_object_full": {"qualname": 2, "fullname": 4, "doc": 14}, "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"qualname": 2, "fullname": 4, "doc": 18}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"qualname": 2, "fullname": 4, "doc": 24}, "TikTokApi.tiktok.TikTokApi.get_hashtag_object": {"qualname": 2, "fullname": 4, "doc": 9}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"qualname": 2, "fullname": 4, "doc": 21}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_id": {"qualname": 2, "fullname": 4, "doc": 10}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_url": {"qualname": 2, "fullname": 4, "doc": 11}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"qualname": 2, "fullname": 4, "doc": 13}, "TikTokApi.tiktok.TikTokApi.get_user_object": {"qualname": 2, "fullname": 4, "doc": 8}, "TikTokApi.tiktok.TikTokApi.get_user": {"qualname": 2, "fullname": 4, "doc": 9}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"qualname": 2, "fullname": 4, "doc": 29}, "TikTokApi.tiktok.TikTokApi.get_video_by_download_url": {"qualname": 2, "fullname": 4, "doc": 16}, "TikTokApi.tiktok.TikTokApi.get_video_by_url": {"qualname": 2, "fullname": 4, "doc": 10}, "TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"qualname": 2, "fullname": 4, "doc": 22}, "TikTokApi.tiktok.TikTokApi.get_music_title": {"qualname": 2, "fullname": 4, "doc": 10}, "TikTokApi.tiktok.TikTokApi.get_secuid": {"qualname": 2, "fullname": 4, "doc": 8}, "TikTokApi.tiktok.TikTokApi.generate_device_id": {"qualname": 2, "fullname": 4, "doc": 9}, "TikTokApi.tiktokuser": {"qualname": 0, "fullname": 2, "doc": 0}, "TikTokApi.tiktokuser.TikTokUser": {"qualname": 1, "fullname": 3, "doc": 0}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"qualname": 2, "fullname": 4, "doc": 25}, "TikTokApi.tiktokuser.TikTokUser.get_insights": {"qualname": 2, "fullname": 4, "doc": 9}, "TikTokApi.utilities": {"qualname": 0, "fullname": 2, "doc": 0}, "TikTokApi.utilities.update_messager": {"qualname": 1, "fullname": 3, "doc": 0}, "TikTokApi.utilities.check": {"qualname": 1, "fullname": 3, "doc": 0}, "TikTokApi.utilities.check_future_deprecation": {"qualname": 1, "fullname": 3, "doc": 0}}, "length": 104, "save": true}, "index": {"qualname": {"root": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.browser_utilities.browser.get_playwright": {"tf": 1}}, "df": 1}}}}}}}}}, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.browser_utilities.browser.browser.get_params": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.get_params": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.get_params": {"tf": 1}}, "df": 3}}}}}, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.browser_utilities.get_acrawler.get_tt_params_script": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_tiktok_by_id": {"tf": 1}}, "df": 1}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_tiktok_by_url": {"tf": 1}}, "df": 1}}}, "h": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.browser_utilities.get_acrawler.get_acrawler": {"tf": 1}}, "df": 1}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktokuser.TikTokUser.get_insights": {"tf": 1}}, "df": 1}}}}}}}, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"TikTokApi.tiktok.TikTokApi.get_data": {"tf": 1}}, "df": 1}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi.tiktok.TikTokApi.get_cookies": {"tf": 1}}, "df": 1}}}}}, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_bytes": {"tf": 1}}, "df": 1}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.tiktok.TikTokApi.get_user": {"tf": 1}}, "df": 1, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1}}, "df": 1}}}, "o": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "j": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_user_object": {"tf": 1}}, "df": 1}}}}}}}}}}}, "m": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "j": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_music_object": {"tf": 1}}, "df": 1, "_": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_music_object_full": {"tf": 1}}, "df": 1, "l": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_music_title": {"tf": 1}}, "df": 1}}}}}}}}}}, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "j": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_hashtag_object": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}}, "df": 1}}}}}}, "d": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_by_download_url": {"tf": 1}}, "df": 1}}}}}}}}}}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_by_url": {"tf": 1}}, "df": 1}}}}}}, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "k": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_secuid": {"tf": 1}}, "df": 1}}}}}}}}, "n": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "p": {"docs": {"TikTokApi.browser_utilities.browser.browser.gen_verifyFp": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.gen_verifyFp": {"tf": 1}}, "df": 2}}}}}}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.exceptions.GenericTikTokError": {"tf": 1}, "TikTokApi.exceptions.GenericTikTokError.__init__": {"tf": 1}}, "df": 2}}}}}}}}}}}}}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.generate_device_id": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}, "b": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.browser_utilities.browser.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.__init__": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.get_params": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.create_context": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.base36encode": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.gen_verifyFp": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.sign_url": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.clean_up": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.find_redirect": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.__init__": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.setup_browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.get_params": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.base36encode": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.gen_verifyFp": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.sign_url": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.clean_up": {"tf": 1}}, "df": 17, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi.browser_utilities.browser_interface.BrowserInterface": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.get_params": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.sign_url": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.clean_up": {"tf": 1}}, "df": 4}}}}}}}}}}}}}}, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"3": {"6": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.browser_utilities.browser.browser.base36encode": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.base36encode": {"tf": 1}}, "df": 2}}}}}}, "docs": {}, "df": 0}, "docs": {}, "df": 0}}}, "y": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}}, "df": 1}}}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}}, "df": 1}}}}}}}, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}}, "df": 1, "_": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1}}, "df": 1}}}}}}}}}, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}}, "df": 1}}}}}}}}}}, "_": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "_": {"docs": {"TikTokApi.browser_utilities.browser.browser.__init__": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.__init__": {"tf": 1}, "TikTokApi.exceptions.TikTokCaptchaError.__init__": {"tf": 1}, "TikTokApi.exceptions.GenericTikTokError.__init__": {"tf": 1}, "TikTokApi.exceptions.TikTokNotFoundError.__init__": {"tf": 1}, "TikTokApi.exceptions.EmptyResponseError.__init__": {"tf": 1}, "TikTokApi.exceptions.JSONDecodeFailure.__init__": {"tf": 1}, "TikTokApi.exceptions.TikTokNotAvailableError.__init__": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.__init__": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}}, "df": 10}}}}}}}}, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.browser_utilities.browser.browser.create_context": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "p": {"docs": {"TikTokApi.browser_utilities.browser.browser.clean_up": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.clean_up": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.clean_up": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.clean_up": {"tf": 1}}, "df": 4}}}}}}}, "h": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.browser_utilities.stealth.chrome_runtime": {"tf": 1}}, "df": 1}}}}}}}}}}}, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {"TikTokApi.utilities.check": {"tf": 1}}, "df": 1, "_": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi.utilities.check_future_deprecation": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.browser_utilities.stealth.console_debug": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.browser_utilities.browser.browser.sign_url": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.sign_url": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.sign_url": {"tf": 1}}, "df": 3}}}}}}}, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.browser_utilities.browser_selenium.browser.setup_browser": {"tf": 1}}, "df": 1}}}}}}}}}, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1}}, "df": 1}}, "m": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1}}, "df": 1}}}, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"TikTokApi.browser_utilities.stealth.stealth": {"tf": 1}}, "df": 1}}}}}}}, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.browser_utilities.browser.browser.find_redirect": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {"TikTokApi.browser_utilities.stealth.iframe_content_window": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi.browser_utilities.stealth.media_codecs": {"tf": 1}}, "df": 1}}}}}}}}}}}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.browser_utilities.stealth.navigator_languages": {"tf": 1}}, "df": 1}}}}}}}, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.browser_utilities.stealth.navigator_permissions": {"tf": 1}}, "df": 1}}}}}}, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi.browser_utilities.stealth.navigator_plugins": {"tf": 1}}, "df": 1}}}}}}, "w": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {"TikTokApi.browser_utilities.stealth.navigator_webdriver": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.browser_utilities.stealth.user_agent": {"tf": 1}}, "df": 1}}, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1}}, "df": 1}}}, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "k": {"docs": {"TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}}, "df": 1, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}}}, "p": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.utilities.update_messager": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "w": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.browser_utilities.stealth.webgl_vendor": {"tf": 1}}, "df": 1}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.browser_utilities.stealth.window_outerdimensions": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.exceptions.TikTokCaptchaError": {"tf": 1}, "TikTokApi.exceptions.TikTokCaptchaError.__init__": {"tf": 1}}, "df": 2}}}}}}}}}}}}, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.exceptions.TikTokNotFoundError": {"tf": 1}, "TikTokApi.exceptions.TikTokNotFoundError.__init__": {"tf": 1}}, "df": 2}}}}}}}}}}, "a": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.exceptions.TikTokNotAvailableError": {"tf": 1}, "TikTokApi.exceptions.TikTokNotAvailableError.__init__": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}}}}, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi.tiktok.TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.__init__": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.clean_up": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_data": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_cookies": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_bytes": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_hashtag_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_id": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_url": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_download_url": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_url": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_title": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_secuid": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.generate_device_id": {"tf": 1}}, "df": 39}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.tiktokuser.TikTokUser": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.get_insights": {"tf": 1}}, "df": 3}}}}}}}}, "e": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.exceptions.EmptyResponseError": {"tf": 1}, "TikTokApi.exceptions.EmptyResponseError.__init__": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}}}}, "x": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "j": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.exceptions.JSONDecodeFailure": {"tf": 1}, "TikTokApi.exceptions.JSONDecodeFailure.__init__": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}}}, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "p": {"docs": {"TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}, "fullname": {"root": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {"TikTokApi.tiktok": {"tf": 1}, "TikTokApi.tiktok.TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.__init__": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.clean_up": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_data": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_cookies": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_bytes": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_hashtag_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_id": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_url": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_download_url": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_url": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_title": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_secuid": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.generate_device_id": {"tf": 1}}, "df": 40, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.browser_utilities": {"tf": 1}, "TikTokApi.browser_utilities.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser.get_playwright": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.__init__": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.get_params": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.create_context": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.base36encode": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.gen_verifyFp": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.sign_url": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.clean_up": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.find_redirect": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.get_params": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.sign_url": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.clean_up": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.__init__": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.setup_browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.get_params": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.base36encode": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.gen_verifyFp": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.sign_url": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.clean_up": {"tf": 1}, "TikTokApi.browser_utilities.get_acrawler": {"tf": 1}, "TikTokApi.browser_utilities.get_acrawler.get_tt_params_script": {"tf": 1}, "TikTokApi.browser_utilities.get_acrawler.get_acrawler": {"tf": 1}, "TikTokApi.browser_utilities.stealth": {"tf": 1}, "TikTokApi.browser_utilities.stealth.chrome_runtime": {"tf": 1}, "TikTokApi.browser_utilities.stealth.console_debug": {"tf": 1}, "TikTokApi.browser_utilities.stealth.iframe_content_window": {"tf": 1}, "TikTokApi.browser_utilities.stealth.media_codecs": {"tf": 1}, "TikTokApi.browser_utilities.stealth.navigator_languages": {"tf": 1}, "TikTokApi.browser_utilities.stealth.navigator_permissions": {"tf": 1}, "TikTokApi.browser_utilities.stealth.navigator_plugins": {"tf": 1}, "TikTokApi.browser_utilities.stealth.navigator_webdriver": {"tf": 1}, "TikTokApi.browser_utilities.stealth.user_agent": {"tf": 1}, "TikTokApi.browser_utilities.stealth.webgl_vendor": {"tf": 1}, "TikTokApi.browser_utilities.stealth.window_outerdimensions": {"tf": 1}, "TikTokApi.browser_utilities.stealth.stealth": {"tf": 1}, "TikTokApi.exceptions": {"tf": 1}, "TikTokApi.exceptions.TikTokCaptchaError": {"tf": 1}, "TikTokApi.exceptions.TikTokCaptchaError.__init__": {"tf": 1}, "TikTokApi.exceptions.GenericTikTokError": {"tf": 1}, "TikTokApi.exceptions.GenericTikTokError.__init__": {"tf": 1}, "TikTokApi.exceptions.TikTokNotFoundError": {"tf": 1}, "TikTokApi.exceptions.TikTokNotFoundError.__init__": {"tf": 1}, "TikTokApi.exceptions.EmptyResponseError": {"tf": 1}, "TikTokApi.exceptions.EmptyResponseError.__init__": {"tf": 1}, "TikTokApi.exceptions.JSONDecodeFailure": {"tf": 1}, "TikTokApi.exceptions.JSONDecodeFailure.__init__": {"tf": 1}, "TikTokApi.exceptions.TikTokNotAvailableError": {"tf": 1}, "TikTokApi.exceptions.TikTokNotAvailableError.__init__": {"tf": 1}, "TikTokApi.tiktok": {"tf": 1}, "TikTokApi.tiktok.TikTokApi": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.__init__": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.clean_up": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_data": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_cookies": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_bytes": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_music_object": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_music_object_full": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_hashtag_object": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_id": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_url": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_user_object": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_user": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_video_by_download_url": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_video_by_url": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_music_title": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_secuid": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.generate_device_id": {"tf": 1.4142135623730951}, "TikTokApi.tiktokuser": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.get_insights": {"tf": 1}, "TikTokApi.utilities": {"tf": 1}, "TikTokApi.utilities.update_messager": {"tf": 1}, "TikTokApi.utilities.check": {"tf": 1}, "TikTokApi.utilities.check_future_deprecation": {"tf": 1}}, "df": 104}}}, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.exceptions.TikTokCaptchaError": {"tf": 1}, "TikTokApi.exceptions.TikTokCaptchaError.__init__": {"tf": 1}}, "df": 2}}}}}}}}}}}}, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.exceptions.TikTokNotFoundError": {"tf": 1}, "TikTokApi.exceptions.TikTokNotFoundError.__init__": {"tf": 1}}, "df": 2}}}}}}}}}}, "a": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.exceptions.TikTokNotAvailableError": {"tf": 1}, "TikTokApi.exceptions.TikTokNotAvailableError.__init__": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.tiktokuser": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser": {"tf": 1.4142135623730951}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1.4142135623730951}, "TikTokApi.tiktokuser.TikTokUser.get_insights": {"tf": 1.4142135623730951}}, "df": 4}}}}}}}}, "b": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.browser_utilities.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser.get_playwright": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser": {"tf": 1.4142135623730951}, "TikTokApi.browser_utilities.browser.browser.__init__": {"tf": 1.4142135623730951}, "TikTokApi.browser_utilities.browser.browser.get_params": {"tf": 1.4142135623730951}, "TikTokApi.browser_utilities.browser.browser.create_context": {"tf": 1.4142135623730951}, "TikTokApi.browser_utilities.browser.browser.base36encode": {"tf": 1.4142135623730951}, "TikTokApi.browser_utilities.browser.browser.gen_verifyFp": {"tf": 1.4142135623730951}, "TikTokApi.browser_utilities.browser.browser.sign_url": {"tf": 1.4142135623730951}, "TikTokApi.browser_utilities.browser.browser.clean_up": {"tf": 1.4142135623730951}, "TikTokApi.browser_utilities.browser.browser.find_redirect": {"tf": 1.4142135623730951}, "TikTokApi.browser_utilities.browser_selenium.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.__init__": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.setup_browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.get_params": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.base36encode": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.gen_verifyFp": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.sign_url": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.clean_up": {"tf": 1}}, "df": 19, "_": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.browser_utilities": {"tf": 1}, "TikTokApi.browser_utilities.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser.get_playwright": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.__init__": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.get_params": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.create_context": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.base36encode": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.gen_verifyFp": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.sign_url": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.clean_up": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser.find_redirect": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.get_params": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.sign_url": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.clean_up": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.__init__": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.setup_browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.get_params": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.base36encode": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.gen_verifyFp": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.sign_url": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.clean_up": {"tf": 1}, "TikTokApi.browser_utilities.get_acrawler": {"tf": 1}, "TikTokApi.browser_utilities.get_acrawler.get_tt_params_script": {"tf": 1}, "TikTokApi.browser_utilities.get_acrawler.get_acrawler": {"tf": 1}, "TikTokApi.browser_utilities.stealth": {"tf": 1}, "TikTokApi.browser_utilities.stealth.chrome_runtime": {"tf": 1}, "TikTokApi.browser_utilities.stealth.console_debug": {"tf": 1}, "TikTokApi.browser_utilities.stealth.iframe_content_window": {"tf": 1}, "TikTokApi.browser_utilities.stealth.media_codecs": {"tf": 1}, "TikTokApi.browser_utilities.stealth.navigator_languages": {"tf": 1}, "TikTokApi.browser_utilities.stealth.navigator_permissions": {"tf": 1}, "TikTokApi.browser_utilities.stealth.navigator_plugins": {"tf": 1}, "TikTokApi.browser_utilities.stealth.navigator_webdriver": {"tf": 1}, "TikTokApi.browser_utilities.stealth.user_agent": {"tf": 1}, "TikTokApi.browser_utilities.stealth.webgl_vendor": {"tf": 1}, "TikTokApi.browser_utilities.stealth.window_outerdimensions": {"tf": 1}, "TikTokApi.browser_utilities.stealth.stealth": {"tf": 1}}, "df": 42}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi.browser_utilities.browser_interface": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.get_params": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.sign_url": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.clean_up": {"tf": 1}}, "df": 5}}}}}}}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.browser_utilities.browser_selenium": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.__init__": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.setup_browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.get_params": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.base36encode": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.gen_verifyFp": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.sign_url": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.clean_up": {"tf": 1}}, "df": 9}}}}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi.browser_utilities.browser_interface.BrowserInterface": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.get_params": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.sign_url": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.clean_up": {"tf": 1}}, "df": 4}}}}}}}}}}}}}}, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"3": {"6": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.browser_utilities.browser.browser.base36encode": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.base36encode": {"tf": 1}}, "df": 2}}}}}}, "docs": {}, "df": 0}, "docs": {}, "df": 0}}}, "y": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}}, "df": 1}}}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}}, "df": 1}}}}}}}, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}}, "df": 1, "_": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1}}, "df": 1}}}}}}}}}, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}}, "df": 1}}}}}}}}}}, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.browser_utilities.browser.get_playwright": {"tf": 1}}, "df": 1}}}}}}}}}, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.browser_utilities.browser.browser.get_params": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.get_params": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.get_params": {"tf": 1}}, "df": 3}}}}}, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.browser_utilities.get_acrawler": {"tf": 1}, "TikTokApi.browser_utilities.get_acrawler.get_tt_params_script": {"tf": 1}, "TikTokApi.browser_utilities.get_acrawler.get_acrawler": {"tf": 1.4142135623730951}}, "df": 3}}}}}}, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.browser_utilities.get_acrawler.get_tt_params_script": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_tiktok_by_id": {"tf": 1}}, "df": 1}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_tiktok_by_url": {"tf": 1}}, "df": 1}}}, "h": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktokuser.TikTokUser.get_insights": {"tf": 1}}, "df": 1}}}}}}}, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"TikTokApi.tiktok.TikTokApi.get_data": {"tf": 1}}, "df": 1}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi.tiktok.TikTokApi.get_cookies": {"tf": 1}}, "df": 1}}}}}, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_bytes": {"tf": 1}}, "df": 1}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.tiktok.TikTokApi.get_user": {"tf": 1}}, "df": 1, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1}}, "df": 1}}}, "o": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "j": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_user_object": {"tf": 1}}, "df": 1}}}}}}}}}}}, "m": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "j": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_music_object": {"tf": 1}}, "df": 1, "_": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_music_object_full": {"tf": 1}}, "df": 1, "l": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_music_title": {"tf": 1}}, "df": 1}}}}}}}}}}, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "j": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_hashtag_object": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}}, "df": 1}}}}}}, "d": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_by_download_url": {"tf": 1}}, "df": 1}}}}}}}}}}}}, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_by_url": {"tf": 1}}, "df": 1}}}}}}, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "k": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_secuid": {"tf": 1}}, "df": 1}}}}}}}}, "n": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "p": {"docs": {"TikTokApi.browser_utilities.browser.browser.gen_verifyFp": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.gen_verifyFp": {"tf": 1}}, "df": 2}}}}}}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.exceptions.GenericTikTokError": {"tf": 1}, "TikTokApi.exceptions.GenericTikTokError.__init__": {"tf": 1}}, "df": 2}}}}}}}}}}}}}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.generate_device_id": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}, "_": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "_": {"docs": {"TikTokApi.browser_utilities.browser.browser.__init__": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.__init__": {"tf": 1}, "TikTokApi.exceptions.TikTokCaptchaError.__init__": {"tf": 1}, "TikTokApi.exceptions.GenericTikTokError.__init__": {"tf": 1}, "TikTokApi.exceptions.TikTokNotFoundError.__init__": {"tf": 1}, "TikTokApi.exceptions.EmptyResponseError.__init__": {"tf": 1}, "TikTokApi.exceptions.JSONDecodeFailure.__init__": {"tf": 1}, "TikTokApi.exceptions.TikTokNotAvailableError.__init__": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.__init__": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}}, "df": 10}}}}}}}}, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.browser_utilities.browser.browser.create_context": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "p": {"docs": {"TikTokApi.browser_utilities.browser.browser.clean_up": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.clean_up": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.clean_up": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.clean_up": {"tf": 1}}, "df": 4}}}}}}}, "h": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.browser_utilities.stealth.chrome_runtime": {"tf": 1}}, "df": 1}}}}}}}}}}}, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {"TikTokApi.utilities.check": {"tf": 1}}, "df": 1, "_": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi.utilities.check_future_deprecation": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.browser_utilities.stealth.console_debug": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.browser_utilities.browser.browser.sign_url": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface.sign_url": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.sign_url": {"tf": 1}}, "df": 3}}}}}}}, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.browser_utilities.browser_selenium.browser.setup_browser": {"tf": 1}}, "df": 1}}}}}}}}}, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1}}, "df": 1}}, "m": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1}}, "df": 1}}}, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"TikTokApi.browser_utilities.stealth": {"tf": 1}, "TikTokApi.browser_utilities.stealth.chrome_runtime": {"tf": 1}, "TikTokApi.browser_utilities.stealth.console_debug": {"tf": 1}, "TikTokApi.browser_utilities.stealth.iframe_content_window": {"tf": 1}, "TikTokApi.browser_utilities.stealth.media_codecs": {"tf": 1}, "TikTokApi.browser_utilities.stealth.navigator_languages": {"tf": 1}, "TikTokApi.browser_utilities.stealth.navigator_permissions": {"tf": 1}, "TikTokApi.browser_utilities.stealth.navigator_plugins": {"tf": 1}, "TikTokApi.browser_utilities.stealth.navigator_webdriver": {"tf": 1}, "TikTokApi.browser_utilities.stealth.user_agent": {"tf": 1}, "TikTokApi.browser_utilities.stealth.webgl_vendor": {"tf": 1}, "TikTokApi.browser_utilities.stealth.window_outerdimensions": {"tf": 1}, "TikTokApi.browser_utilities.stealth.stealth": {"tf": 1.4142135623730951}}, "df": 13}}}}}}}, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.browser_utilities.browser.browser.find_redirect": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {"TikTokApi.browser_utilities.stealth.iframe_content_window": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}}}}, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi.browser_utilities.stealth.media_codecs": {"tf": 1}}, "df": 1}}}}}}}}}}}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.browser_utilities.stealth.navigator_languages": {"tf": 1}}, "df": 1}}}}}}}, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.browser_utilities.stealth.navigator_permissions": {"tf": 1}}, "df": 1}}}}}}, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi.browser_utilities.stealth.navigator_plugins": {"tf": 1}}, "df": 1}}}}}}, "w": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {"TikTokApi.browser_utilities.stealth.navigator_webdriver": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.browser_utilities.stealth.user_agent": {"tf": 1}}, "df": 1}}, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1}}, "df": 1}}}, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "k": {"docs": {"TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}}, "df": 1, "e": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.utilities": {"tf": 1}, "TikTokApi.utilities.update_messager": {"tf": 1}, "TikTokApi.utilities.check": {"tf": 1}, "TikTokApi.utilities.check_future_deprecation": {"tf": 1}}, "df": 4}}}, "p": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.utilities.update_messager": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "w": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.browser_utilities.stealth.webgl_vendor": {"tf": 1}}, "df": 1}}}}}}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.browser_utilities.stealth.window_outerdimensions": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}, "e": {"docs": {}, "df": 0, "x": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.exceptions": {"tf": 1}, "TikTokApi.exceptions.TikTokCaptchaError": {"tf": 1}, "TikTokApi.exceptions.TikTokCaptchaError.__init__": {"tf": 1}, "TikTokApi.exceptions.GenericTikTokError": {"tf": 1}, "TikTokApi.exceptions.GenericTikTokError.__init__": {"tf": 1}, "TikTokApi.exceptions.TikTokNotFoundError": {"tf": 1}, "TikTokApi.exceptions.TikTokNotFoundError.__init__": {"tf": 1}, "TikTokApi.exceptions.EmptyResponseError": {"tf": 1}, "TikTokApi.exceptions.EmptyResponseError.__init__": {"tf": 1}, "TikTokApi.exceptions.JSONDecodeFailure": {"tf": 1}, "TikTokApi.exceptions.JSONDecodeFailure.__init__": {"tf": 1}, "TikTokApi.exceptions.TikTokNotAvailableError": {"tf": 1}, "TikTokApi.exceptions.TikTokNotAvailableError.__init__": {"tf": 1}}, "df": 13}}}}, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 1}}}}}}}}}}}}, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.exceptions.EmptyResponseError": {"tf": 1}, "TikTokApi.exceptions.EmptyResponseError.__init__": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}}}}}, "j": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.exceptions.JSONDecodeFailure": {"tf": 1}, "TikTokApi.exceptions.JSONDecodeFailure.__init__": {"tf": 1}}, "df": 2}}}}}}}}}}}}}}}}, "d": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "p": {"docs": {"TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}, "doc": {"root": {"0": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1}}, "df": 2, ":": {"8": {"0": {"8": {"0": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}}, "1": {"0": {"0": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}, "docs": {"TikTokApi": {"tf": 1}}, "df": 1}, "docs": {"TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1}}, "df": 1}, "2": {"0": {"0": {"0": {"docs": {"TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "8": {"docs": {"TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}}, "df": 4}, "docs": {}, "df": 0, ",": {"0": {"0": {"0": {"docs": {"TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}}, "df": 6}, "docs": {}, "df": 0}, "docs": {}, "df": 0}, "docs": {}, "df": 0}}, "3": {"9": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}, "docs": {}, "df": 0}, "docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}}}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "k": {"docs": {"TikTokApi.tiktok.TikTokApi.get_data": {"tf": 1}}, "df": 1}}}}, "s": {"docs": {"TikTokApi": {"tf": 2}, "TikTokApi.browser_utilities.browser.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.__init__": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 2.449489742783178}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_data": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_music_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_download_url": {"tf": 1}}, "df": 17, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 2}, "TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_user_object": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_user": {"tf": 1.4142135623730951}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1.4142135623730951}}, "df": 11, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1.4142135623730951}}, "df": 3}}, "'": {"docs": {"TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1}}, "df": 5}, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_user_object": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_user": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_secuid": {"tf": 1.7320508075688772}}, "df": 6}}}, "_": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}}, "df": 1}}}}}}}, "_": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}}}}}}}}, "p": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.get_insights": {"tf": 1}}, "df": 9}, "r": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_url": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_video_by_download_url": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_video_by_url": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1.4142135623730951}}, "df": 8}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1.4142135623730951}}, "df": 2}}}}, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1, "i": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "k": {"docs": {"TikTokApi": {"tf": 3.1622776601683795}, "TikTokApi.tiktok.TikTokApi.__init__": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 2.23606797749979}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_data": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 2}, "TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_id": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_url": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 2}, "TikTokApi.tiktok.TikTokApi.get_video_by_download_url": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_video_by_url": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1.7320508075688772}, "TikTokApi.tiktokuser.TikTokUser.get_insights": {"tf": 1}}, "df": 24, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi": {"tf": 2.6457513110645907}, "TikTokApi.tiktok.TikTokApi.__init__": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}}, "df": 4, ":": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}}}}}}}}}}, "'": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_bytes": {"tf": 1}}, "df": 2}}}}, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}, "p": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}, "m": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 2}}, "t": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_music_title": {"tf": 1.4142135623730951}}, "df": 1}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi": {"tf": 2.23606797749979}, "TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}}, "df": 3}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "e": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.4142135623730951}}, "df": 1}}, "i": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "k": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.7320508075688772}}, "df": 1}}}, "h": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "h": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1}}, "df": 2}}}, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "'": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1.4142135623730951}}, "df": 2}}}}, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.4142135623730951}}, "df": 1}}, "r": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}}, "df": 3}}}, "y": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1.4142135623730951}}, "df": 4}}}}, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi": {"tf": 2.8284271247461903}, "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"tf": 1}}, "df": 3}}, "f": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}, "l": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "y": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}}}, "o": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.clean_up": {"tf": 1}}, "df": 1}}}}}}, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1.4142135623730951}}, "df": 3}}}}}, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}}}, "b": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi.browser_utilities.browser.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser": {"tf": 1}}, "df": 3}}, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}}, "df": 4}}}}, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "y": {"docs": {"TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1}}, "df": 1}}}}, "m": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}}, "df": 2}}}}}, "d": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}}}, "n": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}}, "df": 1}}}}}, "p": {"docs": {}, "df": 0, "y": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"3": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}, "docs": {"TikTokApi": {"tf": 2.23606797749979}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 2}}}}}, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}}, "c": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_data": {"tf": 1}}, "df": 4}}}}, "g": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 2}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1.4142135623730951}}, "df": 6, "_": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1}}, "df": 3}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1}}, "df": 1}}}, "r": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}, "a": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.get_insights": {"tf": 1}}, "df": 2, "e": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 3.3166247903554}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 2}, "TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_hashtag_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_id": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_url": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_download_url": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_url": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_title": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_secuid": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}}, "df": 33}}}}}, "s": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_cookies": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.generate_device_id": {"tf": 1}}, "df": 5}}}, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}}, "df": 3}}, "y": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 2.23606797749979}}, "df": 2}}}}}}}}, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}}}}, "r": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1, "o": {"docs": {}, "df": 0, "j": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 2.23606797749979}}, "df": 1}}}}, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}, "TikTokApi.browser_utilities.browser.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser": {"tf": 1}}, "df": 4}}}, "g": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}, "x": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.7320508075688772}}, "df": 1}}}, "e": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "i": {"docs": {}, "df": 0, "x": {"docs": {"TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1.7320508075688772}}, "df": 1}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}}, "df": 3}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1.7320508075688772}}, "df": 1}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}}, "df": 1, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}}, "i": {"docs": {}, "df": 0, "p": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1}}, "df": 2}}}}}, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}}, "df": 9}}}}, "w": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}}, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 3}}, "b": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "h": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_hashtag_object": {"tf": 1}}, "df": 2}}}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "k": {"docs": {"TikTokApi": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 2}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 3}}}, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "k": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "y": {"docs": {"TikTokApi.browser_utilities.browser.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser": {"tf": 1}}, "df": 3}, "n": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 2}, "TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_id": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_url": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1.7320508075688772}}, "df": 9}}, "i": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}, "t": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 2}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "k": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1}}, "df": 1}}}}}}}}, "h": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "v": {"docs": {"TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}}, "df": 1}}}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}}, "df": 2, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}, "m": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}, "TikTokApi.exceptions.TikTokCaptchaError": {"tf": 1}, "TikTokApi.exceptions.GenericTikTokError": {"tf": 1}, "TikTokApi.exceptions.TikTokNotFoundError": {"tf": 1}, "TikTokApi.exceptions.EmptyResponseError": {"tf": 1}, "TikTokApi.exceptions.JSONDecodeFailure": {"tf": 1}, "TikTokApi.exceptions.TikTokNotAvailableError": {"tf": 1}}, "df": 7}}, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}, "/": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, ":": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}}}, "a": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.browser_utilities.browser.browser.base36encode": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.base36encode": {"tf": 1}}, "df": 2}}}}, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 2}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": null}}, "df": 1}}}}}}}, "o": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}}, "df": 1}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 2}, "TikTokApi.tiktok.TikTokApi.clean_up": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 4}}, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1.4142135623730951}}, "df": 13}}}, "o": {"docs": {}, "df": 0, "k": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi.tiktok.TikTokApi.get_cookies": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1.4142135623730951}}, "df": 2}}}, "p": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}}, "df": 1}}}, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.clean_up": {"tf": 1}}, "df": 2}}, "n": {"docs": {}, "df": 0, "'": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "o": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "(": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}}}}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "n": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.browser_utilities.browser.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser": {"tf": 1}, "TikTokApi.exceptions.TikTokCaptchaError": {"tf": 1}, "TikTokApi.exceptions.GenericTikTokError": {"tf": 1}, "TikTokApi.exceptions.TikTokNotFoundError": {"tf": 1}, "TikTokApi.exceptions.EmptyResponseError": {"tf": 1}, "TikTokApi.exceptions.JSONDecodeFailure": {"tf": 1}, "TikTokApi.exceptions.TikTokNotAvailableError": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.__init__": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}}, "df": 13}}}, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.7320508075688772}}, "df": 1}}, "u": {"docs": {}, "df": 0, "p": {"docs": {"TikTokApi.tiktok.TikTokApi.clean_up": {"tf": 1}}, "df": 1}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.browser_utilities.browser.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser": {"tf": 1}}, "df": 4}}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "p": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 3}}}}}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.generate_device_id": {"tf": 1}}, "df": 4}}}}}}}}}}}}}}, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1}}, "df": 4}}}}}}, "f": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "e": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 2}, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1}}, "df": 1}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi": {"tf": 2}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 2}}}, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user": {"tf": 1}}, "df": 2}}, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}, "n": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi.tiktok.TikTokApi.get_cookies": {"tf": 1}}, "df": 1}}}}}, "n": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}}, "df": 1}}}}, "o": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 2}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 1}}}}, "r": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "u": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1}}, "df": 6}}}}, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 2}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 2}}, "df": 4}}, "x": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1}}, "df": 1}, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.generate_device_id": {"tf": 1}}, "df": 1}}}}}, "s": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "f": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_music_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_id": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_secuid": {"tf": 1}}, "df": 12}}}}, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi": {"tf": 2.23606797749979}}, "df": 1}}}}}}, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_music_object": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_music_object_full": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1.4142135623730951}}, "df": 5}}}, "l": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1.7320508075688772}}, "df": 1}}}}, "t": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1}}, "df": 5, "u": {"docs": {}, "df": 0, "p": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 2}}}, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_secuid": {"tf": 1.4142135623730951}}, "df": 4}}}}, "e": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1, "m": {"docs": {"TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}}, "df": 7}}, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}, "c": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {"TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_hashtag_object": {"tf": 1}}, "df": 8, "_": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}}, "df": 4}}}}}}}}}, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}}, "df": 1}}}}}}, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 2.23606797749979}}, "df": 1}}, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.browser_utilities.browser.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 4}}}}}}, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.browser_utilities.browser.browser.base36encode": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.base36encode": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}}, "df": 9}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.7320508075688772}}, "df": 1}}}}, "u": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}}, "df": 8}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "b": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}}, "c": {"docs": {}, "df": 0, "h": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 2}}, "g": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1}}, "df": 1}}}}}}, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 2}}}, "a": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}}, "_": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}}, "df": 2, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}}}}, "t": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}, "g": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1.4142135623730951}}, "df": 2, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 1}}, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 1}}}}}}, "m": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.get_bytes": {"tf": 1}}, "df": 1}}}}, "p": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}}, "df": 1}}}}, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "'": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}}}}}, "y": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_hashtag_object": {"tf": 1}}, "df": 2}}}}}}, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi": {"tf": 2.6457513110645907}}, "df": 1}, "n": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 2}}, "df": 1}}}, "r": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 2}}}}, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktokuser.TikTokUser.get_insights": {"tf": 1}}, "df": 1, "s": {"docs": {}, "df": 0, "/": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktokuser.TikTokUser.get_insights": {"tf": 1}}, "df": 1}}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.__init__": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.4142135623730951}}, "df": 2}}}}, "g": {"docs": {"TikTokApi.browser_utilities.browser.browser.base36encode": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.base36encode": {"tf": 1}}, "df": 2}}}, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.browser_utilities.browser.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser": {"tf": 1}}, "df": 3}}}}}}, "s": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "u": {"docs": {"TikTokApi": {"tf": 3}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 2}}}, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}, "d": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_music_object": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_music_object_full": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_id": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_music_title": {"tf": 1.7320508075688772}, "TikTokApi.tiktokuser.TikTokUser.get_insights": {"tf": 1}}, "df": 11}, "t": {"docs": {}, "df": 0, "'": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_data": {"tf": 1}}, "df": 3}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "f": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 2}}}}, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}}, "df": 1}}}, "p": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}, "'": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}, "m": {"docs": {"TikTokApi": {"tf": 1.7320508075688772}}, "df": 1, "u": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 2}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_music_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_title": {"tf": 1.4142135623730951}}, "df": 6}}}}, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi": {"tf": 2}}, "df": 1}}, "d": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 2, "e": {"docs": {}, "df": 0, "'": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}}}, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}, "h": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 2}}}}, "k": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_data": {"tf": 1}}, "df": 4}}, "n": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}, "i": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}, "t": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "h": {"docs": {"TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}}, "df": 3}}}, "x": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}}, "df": 4}}}}}}, "c": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}}}, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "e": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 2.6457513110645907}, "TikTokApi.tiktok.TikTokApi.clean_up": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.generate_device_id": {"tf": 1}}, "df": 13}}}}}}, "h": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 2, "'": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "p": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 2, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.browser_utilities.browser.browser": {"tf": 1}, "TikTokApi.browser_utilities.browser_interface.BrowserInterface": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser": {"tf": 1}}, "df": 3}}}}}, "a": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_data": {"tf": 1}}, "df": 3}}}, "s": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_hashtag_object": {"tf": 1.7320508075688772}}, "df": 3}}}}}}, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, ":": {"docs": {}, "df": 0, "/": {"docs": {}, "df": 0, "/": {"0": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}}}}}}, "m": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"tf": 1}}, "df": 2}}}}, "b": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "w": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "t": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}, "f": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}, "o": {"docs": {}, "df": 0, "x": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}, "d": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "o": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 2}}}}}}, "i": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}, "y": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}, "u": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1.4142135623730951}}, "df": 2}}}}}}}}, "t": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi.tiktok.TikTokApi.get_bytes": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1}}, "df": 2}}}, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"3": {"6": {"docs": {"TikTokApi.browser_utilities.browser.browser.base36encode": {"tf": 1}, "TikTokApi.browser_utilities.browser_selenium.browser.base36encode": {"tf": 1}}, "df": 2}, "docs": {}, "df": 0}, "docs": {"TikTokApi.exceptions.TikTokCaptchaError": {"tf": 1}, "TikTokApi.exceptions.GenericTikTokError": {"tf": 1}, "TikTokApi.exceptions.TikTokNotFoundError": {"tf": 1}, "TikTokApi.exceptions.EmptyResponseError": {"tf": 1}, "TikTokApi.exceptions.JSONDecodeFailure": {"tf": 1}, "TikTokApi.exceptions.TikTokNotAvailableError": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"tf": 1}}, "df": 7}, "i": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.clean_up": {"tf": 1}}, "df": 2}}}}}, "o": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi": {"tf": 1.7320508075688772}}, "df": 1}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 3.4641016151377544}, "TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}}, "df": 3}}}}}, "u": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}}, "n": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}}, "df": 6, "t": {"docs": {}, "df": 0, "o": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.7320508075688772}}, "df": 1}}}}}}, "f": {"docs": {}, "df": 0, "f": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1}}, "df": 4}}}}}, "b": {"docs": {}, "df": 0, "j": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_music_object": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_music_object_full": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_hashtag_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_id": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_url": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_video_by_download_url": {"tf": 1.4142135623730951}}, "df": 10}}}}}}, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "b": {"docs": {"TikTokApi": {"tf": 1.7320508075688772}}, "df": 1}}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_title": {"tf": 1}}, "df": 4}}}}, "e": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_secuid": {"tf": 1}}, "df": 7, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.__init__": {"tf": 1}}, "df": 2}}}}, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}, "d": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {"TikTokApi.tiktok.TikTokApi.get_cookies": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_bytes": {"tf": 1}}, "df": 2}}}}}}, "n": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 2}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.generate_device_id": {"tf": 1}}, "df": 5, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1, "=": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "u": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}}, "df": 1}}}}}}}}}}}}}}}}}}}}}}}}}}}}, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi": {"tf": 1.7320508075688772}}, "df": 1}}, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}}}}, "o": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "p": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}}}}, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.generate_device_id": {"tf": 1}}, "df": 1}}}}}}}, "f": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}}, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 2}}}}, "p": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1.4142135623730951}}, "df": 1}}}}}, "o": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1, "u": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 2}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}}, "df": 3}}}}}, "k": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi": {"tf": 2}}, "df": 1}}}}, "n": {"docs": {}, "df": 0, "'": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 2}}}, "w": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_download_url": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_video_by_url": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.generate_device_id": {"tf": 1}}, "df": 7, "_": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_by_download_url": {"tf": 1}}, "df": 1}}}}}}}}}}, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "'": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}}, "df": 2}}}}}}, "i": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}}}}}}}, "a": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}, "c": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_id": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_url": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_object": {"tf": 1}}, "df": 12}}}}}}}}, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}, "a": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}}}}, "t": {"docs": {}, "df": 0, "a": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}}, "df": 1}}}, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}}, "u": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1}}, "df": 1}}}, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "n": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 2.449489742783178}}, "df": 1}}}}, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi": {"tf": 2}}, "df": 1}}}}, "v": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 2}}, "s": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1}}, "df": 12}}, "k": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1.4142135623730951}}, "df": 2}}}, "o": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}, "g": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.4142135623730951}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}}, "df": 3, "g": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}}}}}}}}, "o": {"docs": {}, "df": 0, "k": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.get_insights": {"tf": 1}}, "df": 4}}, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "k": {"docs": {"TikTokApi": {"tf": 1.7320508075688772}}, "df": 1, "s": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}}}}, "o": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}}}}, "e": {"docs": {}, "df": 0, "x": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi": {"tf": 2.449489742783178}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1.4142135623730951}}, "df": 2}}}}, "p": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "c": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}}, "o": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.tiktok.TikTokApi.get_user": {"tf": 1}}, "df": 1}}}, "i": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.exceptions.TikTokCaptchaError": {"tf": 1}, "TikTokApi.exceptions.GenericTikTokError": {"tf": 1}, "TikTokApi.exceptions.TikTokNotFoundError": {"tf": 1}, "TikTokApi.exceptions.EmptyResponseError": {"tf": 1}, "TikTokApi.exceptions.JSONDecodeFailure": {"tf": 1}, "TikTokApi.exceptions.TikTokNotAvailableError": {"tf": 1}}, "df": 6}}, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.exceptions.TikTokCaptchaError": {"tf": 1}, "TikTokApi.exceptions.GenericTikTokError": {"tf": 1}, "TikTokApi.exceptions.TikTokNotFoundError": {"tf": 1}, "TikTokApi.exceptions.EmptyResponseError": {"tf": 1}, "TikTokApi.exceptions.JSONDecodeFailure": {"tf": 1}, "TikTokApi.exceptions.TikTokNotAvailableError": {"tf": 1}}, "df": 6}}}}, "e": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 1}}}, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_cookies": {"tf": 1}}, "df": 1}}}}}}, "r": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"tf": 1}}, "df": 7}}}}}}, "t": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 2}}}}}, "r": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi": {"tf": 2}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}}, "df": 3}}, "e": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}}}}}}}, "r": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}}, "df": 2}}}}, "c": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "o": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1}}, "df": 2}}}}}}, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1}}, "df": 1}}}}}}}, "s": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1}}, "df": 3}}}, "p": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "s": {"docs": {"TikTokApi.tiktok.TikTokApi.get_bytes": {"tf": 1}}, "df": 1}}}}}, "a": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "q": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 2.23606797749979}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_data": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1}}, "df": 4, "_": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "y": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}}}}}}}}, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi.tiktok.TikTokApi.get_data": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_bytes": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_music_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_object_full_by_api": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_hashtag_object": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_id": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_url": {"tf": 1}}, "df": 23, "_": {"docs": {}, "df": 0, "b": {"docs": {}, "df": 0, "y": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1}}, "df": 1}}}}}}}, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "v": {"docs": {"TikTokApi.tiktok.TikTokApi.get_tiktok_by_url": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_tiktok_by_html": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_music_title": {"tf": 1}}, "df": 3}}}}}}, "m": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}, "o": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}}}, "a": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1, "g": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "(": {"1": {"9": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}, "docs": {}, "df": 0}, "docs": {}, "df": 0}}}}}, "i": {"docs": {}, "df": 0, "g": {"docs": {}, "df": 0, "h": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}, "y": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "'": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 2}, "l": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 2}}}, "t": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "b": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "r": {"docs": {}, "df": 0, "_": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}}, "df": 1}}}}}}}, "s": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "f": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}}, "df": 1}}}}}}}}, "n": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "t": {"docs": {}, "df": 0, "h": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}, "e": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_trending": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}}, "df": 13}}, "w": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}, "n": {"docs": {"TikTokApi.exceptions.TikTokCaptchaError": {"tf": 1}, "TikTokApi.exceptions.GenericTikTokError": {"tf": 1}, "TikTokApi.exceptions.TikTokNotFoundError": {"tf": 1}, "TikTokApi.exceptions.EmptyResponseError": {"tf": 1}, "TikTokApi.exceptions.JSONDecodeFailure": {"tf": 1}, "TikTokApi.exceptions.TikTokNotAvailableError": {"tf": 1}}, "df": 6}}, "e": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi": {"tf": 2}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 2.23606797749979}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.get_data": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}}, "df": 5}}, "w": {"docs": {"TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}}, "df": 3}}, "a": {"docs": {}, "df": 0, "m": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}, "u": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1, "b": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "r": {"docs": {"TikTokApi.tiktok.TikTokApi.search_for_users": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_music": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.search_for_hashtags": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.discover_type": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_posts": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked_by_username": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_sound_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}}, "df": 13}}}}}}, "v": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1, "i": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {}, "df": 0, "o": {"docs": {"TikTokApi": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 2}, "TikTokApi.tiktok.TikTokApi.external_signer": {"tf": 1.7320508075688772}, "TikTokApi.tiktok.TikTokApi.user_page": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_user_pager": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.user_liked": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_recommended_tiktoks_by_video_id": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_download_url": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_url": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1.4142135623730951}, "TikTokApi.tiktok.TikTokApi.generate_device_id": {"tf": 1}, "TikTokApi.tiktokuser.TikTokUser.get_insights": {"tf": 1}}, "df": 13, "_": {"docs": {}, "df": 0, "u": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "l": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_by_url": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_no_watermark": {"tf": 1}}, "df": 2}}}}, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktokuser.TikTokUser.get_insights": {"tf": 1}}, "df": 1}}}}}}, "a": {"docs": {}, "df": 0, "l": {"docs": {}, "df": 0, "u": {"docs": {"TikTokApi": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_download_url": {"tf": 1}}, "df": 2}, "i": {"docs": {}, "df": 0, "d": {"docs": {"TikTokApi.tiktok.TikTokApi.by_hashtag": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.generate_device_id": {"tf": 1}}, "df": 2}}}}}, "x": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "e": {"docs": {"TikTokApi": {"tf": 1}}, "df": 1}}}}}, "j": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "(": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "n": {"docs": {}, "df": 0, "d": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "m": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}}, "df": 1}}}}}}}}}}, "s": {"docs": {}, "df": 0, "o": {"docs": {}, "df": 0, "n": {"docs": {"TikTokApi.tiktok.TikTokApi.get_data": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_video_by_tiktok": {"tf": 1}}, "df": 2}}}, "a": {"docs": {}, "df": 0, "v": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "s": {"docs": {}, "df": 0, "c": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "i": {"docs": {}, "df": 0, "p": {"docs": {}, "df": 0, "t": {"docs": {"TikTokApi.tiktokuser.TikTokUser.__init__": {"tf": 1}}, "df": 1}}}}}}}}}}, "k": {"docs": {}, "df": 0, "w": {"docs": {}, "df": 0, "a": {"docs": {}, "df": 0, "r": {"docs": {}, "df": 0, "g": {"docs": {"TikTokApi.tiktok.TikTokApi.get_instance": {"tf": 1}, "TikTokApi.tiktok.TikTokApi.get_cookies": {"tf": 1}}, "df": 2}}}}, "e": {"docs": {}, "df": 0, "y": {"docs": {"TikTokApi.tiktok.TikTokApi.get_video_by_download_url": {"tf": 1}}, "df": 1}}}}}}, "pipeline": ["trimmer", "stopWordFilter", "stemmer"], "_isPrebuiltIndex": true} \ No newline at end of file diff --git a/tests/test_by_hashtag.py b/tests/test_by_hashtag.py deleted file mode 100644 index f4adbe4c..00000000 --- a/tests/test_by_hashtag.py +++ /dev/null @@ -1,26 +0,0 @@ -from TikTokApi import TikTokApi -import os - -api = TikTokApi.get_instance( - custom_verifyFp=os.environ.get("verifyFp", None), use_test_endpoints=True -) - -device_id = api.generate_device_id() -trending = api.by_trending(custom_device_id=device_id) - - -def unique_count(tiktoks): - tmp = [] - for t in tiktoks: - if t["id"] not in tmp: - tmp.append(t["id"]) - return tmp - - -def test_hashtag(): - assert len(api.by_hashtag("funny", 5)) == 5 - assert len(api.by_hashtag("funny", 50)) == 50 - - -def test_non_latin1(): - assert len(api.by_hashtag("селфи", count=3)) == 3 diff --git a/tests/test_by_sound.py b/tests/test_by_sound.py deleted file mode 100644 index 4f4d4c29..00000000 --- a/tests/test_by_sound.py +++ /dev/null @@ -1,12 +0,0 @@ -from TikTokApi import TikTokApi -import os - -api = TikTokApi.get_instance( - custom_verifyFp=os.environ.get("verifyFp", None), use_test_endpoints=True -) - - -def test_trending(): - assert abs(len(api.by_sound("6819262113299565318", 5)) - 5) <= 1 - assert abs(len(api.by_sound("6819262113299565318", 10)) - 10) <= 1 - assert abs(len(api.by_sound("6819262113299565318", 20)) - 20) <= 1 diff --git a/tests/test_by_username.py b/tests/test_by_username.py deleted file mode 100644 index 69990d85..00000000 --- a/tests/test_by_username.py +++ /dev/null @@ -1,12 +0,0 @@ -from TikTokApi import TikTokApi -import os - -api = TikTokApi.get_instance( - custom_verifyFp=os.environ.get("verifyFp", None), use_test_endpoints=True -) - - -def test_trending(): - assert abs(len(api.by_username("therock", 5)) - 5) <= 2 - assert abs(len(api.by_username("therock", 10)) - 10) <= 2 - assert abs(len(api.by_username("therock", 20)) - 20) <= 2 diff --git a/tests/test_get_music_object_full_by_api.py b/tests/test_get_music_object_full_by_api.py deleted file mode 100644 index a68cdd64..00000000 --- a/tests/test_get_music_object_full_by_api.py +++ /dev/null @@ -1,12 +0,0 @@ -from TikTokApi import TikTokApi -import os - -api = TikTokApi.get_instance( - custom_verifyFp=os.environ.get("verifyFp", None), use_test_endpoints=True -) - - -def test_get_music_object_full_by_api(): - music_id = "6819262113299565318" - res = api.get_music_object_full_by_api(music_id) - assert res["music"]["id"] == music_id diff --git a/tests/test_get_object_routes.py b/tests/test_get_object_routes.py deleted file mode 100644 index 06c69ee6..00000000 --- a/tests/test_get_object_routes.py +++ /dev/null @@ -1,30 +0,0 @@ -from TikTokApi import TikTokApi -import os - -api = TikTokApi.get_instance( - custom_verifyFp=os.environ.get("verifyFp", None), use_test_endpoints=True -) - - -def test_tiktok_object(): - assert len(api.get_tiktok_by_id("6829267836783971589")) > 0 - assert ( - len( - api.get_tiktok_by_url( - "https://www.tiktok.com/@therock/video/6829267836783971589" - ) - ) - > 0 - ) - - -def test_user_object(): - assert len(api.get_user_object("therock")) > 0 - - -def test_music_object(): - assert len(api.get_music_object("6820695018429253633")) > 0 - - -def test_hashtag_object(): - assert len(api.get_hashtag_object("funny")) > 0 diff --git a/tests/test_hashtag.py b/tests/test_hashtag.py new file mode 100644 index 00000000..5a14f2f4 --- /dev/null +++ b/tests/test_hashtag.py @@ -0,0 +1,28 @@ +from TikTokApi import TikTokApi +import os + +api = TikTokApi(custom_verify_fp=os.environ.get("verifyFp", None)) + + +def test_hashtag_videos(): + tag = api.hashtag(name="funny") + video_count = 0 + for video in tag.videos(count=100): + video_count += 1 + + assert video_count >= 100 + + +def test_hashtag_info(): + tag = api.hashtag(name="funny") + data = tag.info() + assert data["title"] == "funny" + assert data["id"] == "5424" + + +def test_non_latin1(): + name = "селфи" + tag = api.hashtag(name=name) + data = tag.info() + + assert data["title"] == name diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 00000000..253115aa --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,25 @@ +from TikTokApi import TikTokApi +import os + +api = TikTokApi(custom_verify_fp=os.environ.get("verifyFp", None)) + + +def test_video_attributes(): + tag_name = "funny" + for video in api.hashtag(name=tag_name).videos(): + # Test hashtags on video. + tag_included = False + for tag in video.hashtags: + if tag.name == tag_name: + tag_included = True + + assert tag_included + + # Test sound on video. + assert video.sound is not None + assert video.sound.id is not None + + # Test author. + assert video.author is not None + assert video.author.user_id is not None + assert video.author.sec_uid is not None diff --git a/tests/test_search.py b/tests/test_search.py new file mode 100644 index 00000000..91972906 --- /dev/null +++ b/tests/test_search.py @@ -0,0 +1,32 @@ +from TikTokApi import TikTokApi +import os + +api = TikTokApi(custom_verify_fp=os.environ.get("verifyFp", None)) + + +def test_discover_type(): + count = 0 + for result in api.search.users("therock", count=50): + count += 1 + + assert count >= 50 + + count = 0 + for result in api.search.sounds("funny", count=50): + count += 1 + + assert count >= 50 + + count = 0 + for result in api.search.hashtags("funny", count=50): + count += 1 + + assert count >= 50 + + +def test_users_alternate(): + count = 0 + for user in api.search.users_alternate("therock", count=50): + count += 1 + + assert count >= 50 diff --git a/tests/test_search_for.py b/tests/test_search_for.py deleted file mode 100644 index 05a1902f..00000000 --- a/tests/test_search_for.py +++ /dev/null @@ -1,12 +0,0 @@ -from TikTokApi import TikTokApi -import os - -api = TikTokApi.get_instance( - custom_verifyFp=os.environ.get("verifyFp", None), use_test_endpoints=True -) - - -def test_search_for(): - assert len(api.search_for_hashtags("a")) >= 15 - assert len(api.search_for_music("a")) >= 15 - assert len(api.search_for_users("a")) >= 15 diff --git a/tests/test_sound.py b/tests/test_sound.py new file mode 100644 index 00000000..0a85798e --- /dev/null +++ b/tests/test_sound.py @@ -0,0 +1,22 @@ +from TikTokApi import TikTokApi +import os + +api = TikTokApi(custom_verify_fp=os.environ.get("verifyFp", None)) + + +song_id = "7016547803243022337" + + +def test_sound_videos(): + sound = api.sound(id=song_id) + video_count = 0 + for video in sound.videos(count=100): + video_count += 1 + + assert video_count >= 100 + + +def test_sound_info(): + sound = api.sound(id=song_id) + data = sound.info() + assert data["music"]["id"] == song_id diff --git a/tests/test_trending.py b/tests/test_trending.py index c06df328..d134c2d4 100644 --- a/tests/test_trending.py +++ b/tests/test_trending.py @@ -1,12 +1,12 @@ from TikTokApi import TikTokApi import os -api = TikTokApi.get_instance( - custom_verifyFp=os.environ.get("verifyFp", None), use_test_endpoints=True -) +api = TikTokApi(custom_verify_fp=os.environ.get("verifyFp", None)) -def test_trending(): - assert abs(len(api.by_trending(5)) - 5) <= 2 - assert abs(len(api.by_trending(10)) - 10) <= 2 - assert abs(len(api.by_trending(20)) - 20) <= 2 +def test_trending_videos(): + count = 0 + for video in api.trending.videos(count=100): + count += 1 + + assert count >= 100 diff --git a/tests/test_user.py b/tests/test_user.py index 10d79a6d..75511105 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -1,52 +1,41 @@ from TikTokApi import TikTokApi import os -api = TikTokApi.get_instance( - custom_verifyFp=os.environ.get("verifyFp", None), use_test_endpoints=True -) - - -def test_user(): - assert ( - api.get_user("charlidamelio")["userInfo"]["user"]["uniqueId"] == "charlidamelio" - ) - assert api.get_user_object("charlidamelio")["uniqueId"] == "charlidamelio" - assert ( - abs( - len( - api.user_posts( - userID="5058536", - secUID="MS4wLjABAAAAoRsCq3Yj6BtSKBCQ4rf3WQYxIaxe5VetwJfYzW_U5K8", - count=5, - ) - ) - - 5 - ) - <= 1 - ) - assert ( - abs( - len( - api.user_posts( - userID="5058536", - secUID="MS4wLjABAAAAoRsCq3Yj6BtSKBCQ4rf3WQYxIaxe5VetwJfYzW_U5K8", - count=10, - ) - ) - - 10 - ) - <= 1 - ) - assert ( - abs( - len( - api.user_posts( - userID="5058536", - secUID="MS4wLjABAAAAoRsCq3Yj6BtSKBCQ4rf3WQYxIaxe5VetwJfYzW_U5K8", - count=30, - ) - ) - - 30 - ) - <= 1 - ) +api = TikTokApi(custom_verify_fp=os.environ.get("verifyFp", None)) + + +username = "charlidamelio" +user_id = "5831967" +sec_uid = "MS4wLjABAAAA-VASjiXTh7wDDyXvjk10VFhMWUAoxr8bgfO1kAL1-9s" + + +def test_user_info(): + data = api.user(username=username).info() + + assert data["uniqueId"] == username + assert data["id"] == user_id + assert data["secUid"] == sec_uid + + +def test_user_videos(): + count = 0 + for video in api.user(username=username).videos(count=100): + count += 1 + + assert count >= 100 + + count = 0 + for video in api.user(user_id=user_id, sec_uid=sec_uid).videos(count=100): + count += 1 + + assert count >= 100 + + +def test_user_liked(): + user = api.user(username="public_likes") + + count = 0 + for v in user.liked(): + count += 1 + + assert count >= 1 diff --git a/tests/test_user_pager.py b/tests/test_user_pager.py deleted file mode 100644 index b0e2b1fb..00000000 --- a/tests/test_user_pager.py +++ /dev/null @@ -1,55 +0,0 @@ -from TikTokApi import TikTokApi -import os - -api = TikTokApi.get_instance( - custom_verifyFp=os.environ.get("verifyFp", None), use_test_endpoints=True -) - - -class TestUserPager: - """Test the pager returned by getUserPager""" - - def test_page_size(self): - """Pages should be pretty close to the specified size""" - - pager = api.get_user_pager("therock", page_size=5) - - page = pager.__next__() - assert abs(len(page) - 5) <= 2 - - page = pager.__next__() - assert abs(len(page) - 5) <= 2 - - def test_user_pager_before(self): - """Should always request therock's first 19 tiktoks across 2 pages""" - APR_24 = 1587757436000 # 2020-04-24 15:43:56 to be precise. Must be ms-precision timestamp - - pager = api.get_user_pager("therock", page_size=10, cursor=APR_24) - - total_tts = 0 - pages = 0 - for page in pager: - pages += 1 - total_tts += len(page) - - assert pages == 2 - assert total_tts == 19 - - # TikTokApi No Longer Supports - '''def test_user_pager_before_after(self): - """Should always request the 7 tiktoks between those times""" - APR_24 = 1587757437000 # 2020-04-24 15:43:57 - AUG_10 = 1597076218000 # 2020-08-10 12:16:58 - - pager = api.getUserPager( - "therock", page_size=3, cursor=APR_24, maxCursor=AUG_10 - ) - - total_tts = 0 - pages = 0 - for page in pager: - pages += 1 - total_tts += len(page) - - assert pages == 3 - assert total_tts == 7''' diff --git a/tests/test_video.py b/tests/test_video.py new file mode 100644 index 00000000..46b8591a --- /dev/null +++ b/tests/test_video.py @@ -0,0 +1,36 @@ +from TikTokApi import TikTokApi +import os + +api = TikTokApi(custom_verify_fp=os.environ.get("verifyFp", None)) + + +def test_video_id_from_url(): + url = "https://www.tiktok.com/@therock/video/7041997751718137094?is_copy_url=1&is_from_webapp=v1" + video = api.video(url=url) + + assert video.id == "7041997751718137094" + + mobile_url = "https://vm.tiktok.com/TTPdhJDvej" + video = api.video(url=mobile_url) + + assert video.id == "7041997751718137094" + + pass + + +def test_video_info(): + video_id = "7041997751718137094" + video = api.video(id=video_id) + + data = video.info() + + assert data["id"] == video_id + + +def test_video_bytes(): + video_id = "7041997751718137094" + video = api.video(id=video_id) + + data = video.bytes() + + assert len(data) > 10000 From 427fca6c7fa58b9462b1e4cea01889f92a37adc3 Mon Sep 17 00:00:00 2001 From: davidteather Date: Sun, 23 Jan 2022 18:58:37 -0600 Subject: [PATCH 04/17] update tests & auto call object --- TikTokApi/api/hashtag.py | 21 +- TikTokApi/api/sound.py | 10 +- TikTokApi/api/user.py | 12 +- TikTokApi/api/video.py | 13 +- .../browser_utilities/browser_selenium.py | 224 ------------------ TikTokApi/tiktok.py | 14 +- examples/demo_user_pager.py | 49 ---- examples/discover.py | 15 -- examples/download_tiktok.py | 19 -- examples/external_signer.py | 57 ----- examples/get_a_users_videos.py | 20 -- examples/get_tiktoks_by_hashtag.py | 10 - examples/get_tiktoks_by_sound.py | 13 - examples/get_tiktoks_by_username.py | 10 - examples/get_trending.py | 17 -- examples/hashtag_example.py | 11 + examples/search_example.py | 14 ++ examples/sound_example.py | 10 + examples/trending_example.py | 7 + examples/user_example.py | 12 + examples/user_id_crawler.py | 12 - examples/video_example.py | 12 + 22 files changed, 119 insertions(+), 463 deletions(-) delete mode 100644 TikTokApi/browser_utilities/browser_selenium.py delete mode 100644 examples/demo_user_pager.py delete mode 100644 examples/discover.py delete mode 100644 examples/download_tiktok.py delete mode 100644 examples/external_signer.py delete mode 100644 examples/get_a_users_videos.py delete mode 100644 examples/get_tiktoks_by_hashtag.py delete mode 100644 examples/get_tiktoks_by_sound.py delete mode 100644 examples/get_tiktoks_by_username.py delete mode 100644 examples/get_trending.py create mode 100644 examples/hashtag_example.py create mode 100644 examples/search_example.py create mode 100644 examples/sound_example.py create mode 100644 examples/trending_example.py create mode 100644 examples/user_example.py delete mode 100644 examples/user_id_crawler.py create mode 100644 examples/video_example.py diff --git a/TikTokApi/api/hashtag.py b/TikTokApi/api/hashtag.py index f4a5047c..cbf605a1 100644 --- a/TikTokApi/api/hashtag.py +++ b/TikTokApi/api/hashtag.py @@ -4,7 +4,7 @@ from urllib.parse import urlencode from ..exceptions import * -from typing import TYPE_CHECKING, Generator, Optional +from typing import TYPE_CHECKING, ClassVar, Generator, Optional if TYPE_CHECKING: from ..tiktok import TikTokApi @@ -12,7 +12,11 @@ class Hashtag: - parent: TikTokApi + parent: ClassVar[TikTokApi] + + id: str + name: str + as_dict: dict def __init__( self, @@ -20,10 +24,11 @@ def __init__( id: Optional[str] = None, data: Optional[str] = None, ): - self.as_dict = data self.name = name self.id = id + if data is not None: + self.as_dict = data self.__extract_from_data() def info(self, **kwargs) -> dict: @@ -46,7 +51,7 @@ def info_full(self, **kwargs) -> dict: ) = self.parent._process_kwargs(kwargs) kwargs["custom_device_id"] = device_id - if self.name: + if self.name is not None: query = {"challengeName": self.name} else: query = {"challengeId": self.id} @@ -121,3 +126,11 @@ def __repr__(self): def __str__(self): return f"TikTokApi.hashtag(id='{self.id}', name='{self.name}')" + + def __getattr__(self, name): + if name in ["id", "name", "as_dict"]: + self.as_dict = self.info() + self.__extract_from_data() + return self.__getattribute__(name) + + raise AttributeError(f"{name} doesn't exist on TikTokApi.api.Hashtag") diff --git a/TikTokApi/api/sound.py b/TikTokApi/api/sound.py index d0943d1f..b225e709 100644 --- a/TikTokApi/api/sound.py +++ b/TikTokApi/api/sound.py @@ -26,8 +26,8 @@ class Sound: author: Optional[User] def __init__(self, id: Optional[str] = None, data: Optional[str] = None): - self.as_dict = data if data is not None: + self.as_dict = data self.__extract_from_data() elif id is None: raise TypeError("You must provide id parameter.") @@ -133,3 +133,11 @@ def __repr__(self): def __str__(self): return f"TikTokApi.sound(id='{self.id}')" + + def __getattr__(self, name): + if name in ["title", "author", "as_dict"]: + self.as_dict = self.info() + self.__extract_from_data() + return self.__getattribute__(name) + + raise AttributeError(f"{name} doesn't exist on TikTokApi.api.Sound") diff --git a/TikTokApi/api/user.py b/TikTokApi/api/user.py index 6717967b..4148c611 100644 --- a/TikTokApi/api/user.py +++ b/TikTokApi/api/user.py @@ -31,7 +31,7 @@ class User: user_id: str sec_uid: str username: str - data: dict + as_dict: dict def __init__( self, @@ -41,8 +41,8 @@ def __init__( data: Optional[str] = None, ): self.__update_id_sec_uid_username(user_id, sec_uid, username) - self.as_dict = data if data is not None: + self.as_dict = data self.__extract_from_data() def info(self, **kwargs): @@ -247,3 +247,11 @@ def __repr__(self): def __str__(self): return f"TikTokApi.user(username='{self.username}', user_id='{self.user_id}', sec_uid='{self.sec_uid}')" + + def __getattr__(self, name): + if name in ["as_dict"]: + self.as_dict = self.info() + self.__extract_from_data() + return self.__getattribute__(name) + + raise AttributeError(f"{name} doesn't exist on TikTokApi.api.User") diff --git a/TikTokApi/api/video.py b/TikTokApi/api/video.py index f11c35e7..e2151ffa 100644 --- a/TikTokApi/api/video.py +++ b/TikTokApi/api/video.py @@ -25,6 +25,8 @@ class Video: parent: ClassVar[TikTokApi] + # TODO: Use __getattribute__ + id: str author: Optional[User] sound: Optional[Sound] @@ -38,8 +40,8 @@ def __init__( data: Optional[dict] = None, ): self.id = id - self.as_dict = data if data is not None: + self.as_dict = data self.__extract_from_data() elif url is not None: self.id = extract_video_id_from_url(url) @@ -110,3 +112,12 @@ def __repr__(self): def __str__(self): return f"TikTokApi.video(id='{self.id}')" + + def __getattr__(self, name): + # Handle author, sound, hashtags, as_dict + if name in ["author", "sound", "hashtags", "as_dict"]: + self.as_dict = self.info() + self.__extract_from_data() + return self.__getattribute__(name) + + raise AttributeError(f"{name} doesn't exist on TikTokApi.api.Video") diff --git a/TikTokApi/browser_utilities/browser_selenium.py b/TikTokApi/browser_utilities/browser_selenium.py deleted file mode 100644 index e8229464..00000000 --- a/TikTokApi/browser_utilities/browser_selenium.py +++ /dev/null @@ -1,224 +0,0 @@ -import random -import time -import requests -import logging -from threading import Thread -import time -import re -import random -import json -from .browser_interface import BrowserInterface -from selenium_stealth import stealth -from selenium import webdriver -from .get_acrawler import get_acrawler, get_tt_params_script -from urllib.parse import splitquery, parse_qs, parse_qsl - - -class browser(BrowserInterface): - def __init__( - self, - **kwargs, - ): - self.kwargs = kwargs - self.debug = kwargs.get("debug", False) - self.proxy = kwargs.get("proxy", None) - self.api_url = kwargs.get("api_url", None) - self.referrer = kwargs.get("referer", "https://www.tiktok.com/") - self.language = kwargs.get("language", "en") - self.executablePath = kwargs.get("executablePath", "chromedriver") - self.device_id = kwargs.get("custom_device_id", None) - - args = kwargs.get("browser_args", []) - options = kwargs.get("browser_options", {}) - - if len(args) == 0: - self.args = [] - else: - self.args = args - - options = webdriver.ChromeOptions() - options.add_argument("--headless") - options.add_argument("log-level=2") - self.options = { - "headless": True, - "handleSIGINT": True, - "handleSIGTERM": True, - "handleSIGHUP": True, - } - - if self.proxy is not None: - if "@" in self.proxy: - server_prefix = self.proxy.split("://")[0] - address = self.proxy.split("@")[1] - self.options["proxy"] = { - "server": server_prefix + "://" + address, - "username": self.proxy.split("://")[1].split(":")[0], - "password": self.proxy.split("://")[1].split("@")[0].split(":")[1], - } - else: - self.options["proxy"] = {"server": self.proxy} - - # self.options.update(options) - - if self.executablePath is not None: - self.options["executablePath"] = self.executablePath - - try: - self.browser = webdriver.Chrome( - executable_path=self.executablePath, chrome_options=options - ) - except Exception as e: - raise e - - # Page avoidance - self.setup_browser() - # page.close() - - def setup_browser(self): - stealth( - self.browser, - languages=["en-US", "en"], - vendor="Google Inc.", - platform="Win32", - webgl_vendor="Intel Inc.", - renderer="Intel Iris OpenGL Engine", - fix_hairline=True, - ) - - self.get_params(self.browser) - # NOTE: Slower than playwright at loading this because playwright can ignore unneeded files. - self.browser.get("https://www.tiktok.com/@redbull") - self.browser.execute_script(get_acrawler()) - self.browser.execute_script(get_tt_params_script()) - - def get_params(self, page) -> None: - self.user_agent = page.execute_script("""return navigator.userAgent""") - self.browser_language = self.kwargs.get( - "browser_language", ("""return navigator.language""") - ) - self.browser_version = """return window.navigator.appVersion""" - - if len(self.browser_language.split("-")) == 0: - self.region = self.kwargs.get("region", "US") - self.language = self.kwargs.get("language", "en") - elif len(self.browser_language.split("-")) == 1: - self.region = self.kwargs.get("region", "US") - self.language = self.browser_language.split("-")[0] - else: - self.region = self.kwargs.get("region", self.browser_language.split("-")[1]) - self.language = self.kwargs.get( - "language", self.browser_language.split("-")[0] - ) - - self.timezone_name = self.kwargs.get( - "timezone_name", - ("""return Intl.DateTimeFormat().resolvedOptions().timeZone"""), - ) - self.width = """return screen.width""" - self.height = """return screen.height""" - - def base36encode(self, number, alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"): - """Converts an integer to a base36 string.""" - base36 = "" - sign = "" - - if number < 0: - sign = "-" - number = -number - - if 0 <= number < len(alphabet): - return sign + alphabet[number] - - while number != 0: - number, i = divmod(number, len(alphabet)) - base36 = alphabet[i] + base36 - - return sign + base36 - - def gen_verifyFp(self): - chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[:] - chars_len = len(chars) - scenario_title = self.base36encode(int(time.time() * 1000)) - uuid = [0] * 36 - uuid[8] = "_" - uuid[13] = "_" - uuid[18] = "_" - uuid[23] = "_" - uuid[14] = "4" - - for i in range(36): - if uuid[i] != 0: - continue - r = int(random.random() * chars_len) - uuid[i] = chars[int((3 & r) | 8 if i == 19 else r)] - - return f'verify_{scenario_title.lower()}_{"".join(uuid)}' - - def sign_url(self, calc_tt_params=False, **kwargs): - url = kwargs.get("url", None) - if url is None: - raise Exception("sign_url required a url parameter") - - tt_params = None - if kwargs.get("gen_new_verifyFp", False): - verifyFp = self.gen_verifyFp() - else: - verifyFp = kwargs.get( - "custom_verify_fp", - "verify_khgp4f49_V12d4mRX_MdCO_4Wzt_Ar0k_z4RCQC9pUDpX", - ) - - if kwargs.get("custom_device_id") is not None: - device_id = kwargs.get("custom_device_id", None) - elif self.device_id is None: - device_id = str(random.randint(10000, 999999999)) - else: - device_id = self.device_id - - url = "{}&verifyFp={}&device_id={}".format(url, verifyFp, device_id) - # self.browser.execute_script(content=get_acrawler()) - # Should be covered by an earlier addition of get_acrawler. - evaluatedPage = ( - self.browser.execute_script( - ''' - var url = "''' - + url - + """" - var token = window.byted_acrawler.sign({url: url}); - return token; - """ - ), - ) - - url = "{}&_signature={}".format(url, evaluatedPage) - # self.browser.execute_script(content=get_tt_params_script()) - # Should be covered by an earlier addition of get_acrawler. - - tt_params = self.browser.execute_script( - """() => { - return window.genXTTParams(""" - + json.dumps(dict(parse_qsl(splitquery(url)[1]))) - + """); - - }""" - ) - - return (verifyFp, device_id, evaluatedPage, tt_params) - - def clean_up(self): - try: - self.browser.close() - except: - logging.warning("cleanup of browser failed") - - def __format_proxy(self, proxy): - if proxy is not None: - return {"http": proxy, "https": proxy} - else: - return None - - def __get_js(self): - return requests.get( - "https://sf16-muse-va.ibytedtos.com/obj/rc-web-sdk-gcs/acrawler.js", - proxies=self.__format_proxy(self.proxy), - ).text diff --git a/TikTokApi/tiktok.py b/TikTokApi/tiktok.py index a1efac26..d279ad67 100644 --- a/TikTokApi/tiktok.py +++ b/TikTokApi/tiktok.py @@ -12,6 +12,7 @@ from .exceptions import * from .utilities import update_messager +from .browser_utilities.browser import browser os.environ["no_proxy"] = "127.0.0.1,localhost" @@ -23,7 +24,7 @@ class TikTokApi: _instance = None @staticmethod - def __new__(cls, *args, **kwargs): + def __new__(cls, logging_level=logging.WARNING, *args, **kwargs): """The TikTokApi class. Used to interact with TikTok. This is a singleton class to prevent issues from arising with playwright @@ -88,10 +89,10 @@ class to prevent issues from arising with playwright if cls._instance is None: cls._instance = super(TikTokApi, cls).__new__(cls) - cls._instance._initialize(*args, **kwargs) + cls._instance._initialize(logging_level=logging_level, *args, **kwargs) return cls._instance - def _initialize(self, **kwargs): + def _initialize(self, logging_level=logging.WARNING, **kwargs): # Add classes from the api folder user.User.parent = self self.user = user.User @@ -111,8 +112,7 @@ def _initialize(self, **kwargs): trending.Trending.parent = self self.trending = trending.Trending - logging.basicConfig(level=kwargs.get("logging_level", logging.WARNING)) - logging.info("Class initalized") + logging.basicConfig(level=logging_level) # Some Instance Vars self.executablePath = kwargs.get("executablePath", None) @@ -130,10 +130,6 @@ def _initialize(self, **kwargs): if kwargs.get("use_test_endpoints", False): global BASE_URL BASE_URL = "https://t.tiktok.com/" - if kwargs.get("use_selenium", False): - from .browser_utilities.browser_selenium import browser - else: - from .browser_utilities.browser import browser if kwargs.get("generate_static_device_id", False): self.custom_device_id = "".join( diff --git a/examples/demo_user_pager.py b/examples/demo_user_pager.py deleted file mode 100644 index f6a418ae..00000000 --- a/examples/demo_user_pager.py +++ /dev/null @@ -1,49 +0,0 @@ -from datetime import datetime -from TikTokApi import TikTokApi - -api = TikTokApi(debug=True) - - -def printPage(page): - """Just prints out each post with timestamp and description""" - for post in page: - print("{}: {}".format(datetime.fromtimestamp(post["createTime"]), post["desc"])) - - -count = 20 -username = "therock" - -# count and list all of the posts for a given user with the pager -total = 0 - -pager = api.get_user_pager(username, page_size=count) - -for page in pager: - printPage(page) - total += len(page) - -print("{} has {} posts".format(username, total)) -all_posts = total - -# List all of the posts for a given user after a certain date - -APR_24 = 1587757438000 # 2020-04-24 15:43:58 to be precise. Must be ms-precision UNIX timestamp -user = api.get_user_object(username) -page = api.user_page(user["id"], user["secUid"], page_size=30, after=APR_24) - -printPage(page["itemList"]) -new_posts = len(page["itemList"]) -print("{} has {} posts after {}".format(username, new_posts, APR_24)) - - -# Count and list all of the posts before a certain date for a given user with the pager - -total = 0 -pager = api.get_user_pager(username, page_size=count, before=APR_24) - -for page in pager: - printPage(page) - total += len(page) - -print("{} has {} posts from before {}".format(username, total, APR_24)) -print("Should be {}".format(all_posts - new_posts)) diff --git a/examples/discover.py b/examples/discover.py deleted file mode 100644 index e7b3ecbc..00000000 --- a/examples/discover.py +++ /dev/null @@ -1,15 +0,0 @@ -from TikTokApi import TikTokApi - -api = TikTokApi.get_instance() - -# Gets array of trending music objects -trendingMusic = api.discover_music() - -for tiktok in trendingMusic: - print(tiktok) - -# Gets array of trending challenges (hashtags) -trendingChallenges = api.discover_hashtags() - -for tiktok in trendingChallenges: - print(tiktok) diff --git a/examples/download_tiktok.py b/examples/download_tiktok.py deleted file mode 100644 index 744635ed..00000000 --- a/examples/download_tiktok.py +++ /dev/null @@ -1,19 +0,0 @@ -from TikTokApi import TikTokApi -import random - -# SEE https://github.com/davidteather/TikTok-Api/issues/311#issuecomment-721164493 - -# Starts TikTokApi -api = TikTokApi.get_instance() - -# This is generating the tt_webid_v2 cookie -# need to pass it to methods you want to download -device_id = api.generate_device_id() - -trending = api.by_trending(custom_device_id=device_id) - -# Below is if the method used if you have the full tiktok object -video_bytes = api.get_video_by_tiktok(trending[0], custom_device_id=device_id) - -with open("video.mp4", "wb") as out: - out.write(video_bytes) diff --git a/examples/external_signer.py b/examples/external_signer.py deleted file mode 100644 index e4b5cc3e..00000000 --- a/examples/external_signer.py +++ /dev/null @@ -1,57 +0,0 @@ -import random -from TikTokApi import browser -from TikTokApi import TikTokApi -from flask import Flask, request, jsonify -from gevent import monkey - -monkey.patch_all() - - -proxy = None - -signing_browser = browser.browser(proxy=proxy) - -app = Flask(__name__) - - -@app.route("/sign", methods=["GET"]) -def sign_url(): - url = request.args.get("url") - device_id = request.args.get("custom_device_id", None) - if url is None: - return jsonify({"success": False, "error": "You must provide a URL"}) - - if device_id is not None: - device_id = str(random.randint(10000, 999999999)) - verifyFp, device_id, _signature = signing_browser.sign_url( - url=url, custom_device_id=device_id - ) - - return jsonify( - { - "verifyFp": verifyFp, - "device_id": device_id, - "_signature": _signature, - "userAgent": signing_browser.userAgent, - "referrer": signing_browser.referrer, - } - ) - - -if __name__ == "__main__": - app.run(debug=False, port=5000, host="0.0.0.0", threaded=True) - print("Cleaning Up") - try: - signing_browser.clean_up() - except Exception: - pass - try: - browser.get_playwright().stop() - except Exception: - pass - -# Example script below for production use - -# from TikTokApi import TikTokApi -# api = TikTokApi(external_signer="http://localhost:5000/sign") -# print(api.trending()) diff --git a/examples/get_a_users_videos.py b/examples/get_a_users_videos.py deleted file mode 100644 index 770ee5c5..00000000 --- a/examples/get_a_users_videos.py +++ /dev/null @@ -1,20 +0,0 @@ -from TikTokApi import TikTokApi - -# Starts TikTokApi -api = TikTokApi.get_instance() - -# The Number of trending TikToks you want to be displayed -results = 10 - -# Returns a list of dictionaries of the trending object -userPosts = api.user_posts( - "6745191554350760966", - "MS4wLjABAAAAM3R2BtjzVT-uAtstkl2iugMzC6AtnpkojJbjiOdDDrdsTiTR75-8lyWJCY5VvDrZ", - 30, -) -# Loops over every tiktok -for tiktok in userPosts: - # Prints the text of the tiktok - print(tiktok["desc"]) - -print(len(userPosts)) diff --git a/examples/get_tiktoks_by_hashtag.py b/examples/get_tiktoks_by_hashtag.py deleted file mode 100644 index a3a57d0d..00000000 --- a/examples/get_tiktoks_by_hashtag.py +++ /dev/null @@ -1,10 +0,0 @@ -from TikTokApi import TikTokApi - -api = TikTokApi.get_instance() - -count = 30 - -tiktoks = api.by_hashtag("funny", count=count) - -for tiktok in tiktoks: - print(tiktok) diff --git a/examples/get_tiktoks_by_sound.py b/examples/get_tiktoks_by_sound.py deleted file mode 100644 index 31c5aa8d..00000000 --- a/examples/get_tiktoks_by_sound.py +++ /dev/null @@ -1,13 +0,0 @@ -from TikTokApi import TikTokApi - -api = TikTokApi.get_instance() - -count = 30 - -# You can find this from a tiktok getting method in another way or find songs from the discoverMusic method. -sound_id = "6601861313180207878" - -tiktoks = api.by_sound(sound_id, count=count) - -for tiktok in tiktoks: - print(tiktok) diff --git a/examples/get_tiktoks_by_username.py b/examples/get_tiktoks_by_username.py deleted file mode 100644 index e08d7fcf..00000000 --- a/examples/get_tiktoks_by_username.py +++ /dev/null @@ -1,10 +0,0 @@ -from TikTokApi import TikTokApi - -api = TikTokApi.get_instance() - -count = 30 - -tiktoks = api.by_username("americanredcross", count=count) - -for tiktok in tiktoks: - print(tiktok) diff --git a/examples/get_trending.py b/examples/get_trending.py deleted file mode 100644 index 6e3c92f5..00000000 --- a/examples/get_trending.py +++ /dev/null @@ -1,17 +0,0 @@ -from TikTokApi import TikTokApi - -# Starts TikTokApi -api = TikTokApi.get_instance() - -# The Number of trending TikToks you want to be displayed -results = 10 - -# Returns a list of dictionaries of the trending object -trending = api.by_trending(results) - -# Loops over every tiktok -for tiktok in trending: - # Prints the text of the tiktok - print(tiktok["desc"]) - -print(len(trending)) diff --git a/examples/hashtag_example.py b/examples/hashtag_example.py new file mode 100644 index 00000000..b8b52c46 --- /dev/null +++ b/examples/hashtag_example.py @@ -0,0 +1,11 @@ +from TikTokApi import TikTokApi + +verify_fp = "verify_xxx" +api = TikTokApi(custom_verify_fp=verify_fp) + +tag = api.hashtag(name="funny") + +print(tag.info()) + +for video in tag.videos(): + print(video.id) diff --git a/examples/search_example.py b/examples/search_example.py new file mode 100644 index 00000000..89a8991f --- /dev/null +++ b/examples/search_example.py @@ -0,0 +1,14 @@ +from TikTokApi import TikTokApi + +verify_fp = "verify_xxx" +api = TikTokApi(custom_verify_fp=verify_fp) + + +for user in api.search.users("therock"): + print(user.username) + +for sound in api.search.sounds("funny"): + print(sound.title) + +for hashtag in api.search.hashtags("funny"): + print(hashtag.name) diff --git a/examples/sound_example.py b/examples/sound_example.py new file mode 100644 index 00000000..db51bc18 --- /dev/null +++ b/examples/sound_example.py @@ -0,0 +1,10 @@ +from TikTokApi import TikTokApi + +verify_fp = "verify_xxx" +api = TikTokApi(custom_verify_fp=verify_fp) + +sound = api.sound(id='7016547803243022337') + +for video in sound.videos(): + print(video.id) + diff --git a/examples/trending_example.py b/examples/trending_example.py new file mode 100644 index 00000000..001104d0 --- /dev/null +++ b/examples/trending_example.py @@ -0,0 +1,7 @@ +from TikTokApi import TikTokApi + +verify_fp = "verify_xxx" +api = TikTokApi(custom_verify_fp=verify_fp) + +for video in api.trending.videos(): + print(video.id) diff --git a/examples/user_example.py b/examples/user_example.py new file mode 100644 index 00000000..79c75f38 --- /dev/null +++ b/examples/user_example.py @@ -0,0 +1,12 @@ +from TikTokApi import TikTokApi + +verify_fp = "verify_xxx" +api = TikTokApi(custom_verify_fp=verify_fp) + +user = api.user(username="therock") + +for video in user.videos(): + print(video.id) + +for liked_video in api.user(username="public_likes").videos(): + print(liked_video.id) diff --git a/examples/user_id_crawler.py b/examples/user_id_crawler.py deleted file mode 100644 index 03aaf761..00000000 --- a/examples/user_id_crawler.py +++ /dev/null @@ -1,12 +0,0 @@ -from TikTokApi import TikTokApi - -api = TikTokApi.get_instance() - -# If you want to get a variety of users from suggested users you need to provide various -# userIds to the getSuggestedUsersbyID function. So here's an example to do that. - -usersToCrawl = 50 - -userId = "6745191554350760966" # This is therock's userId for TikTok you can change it to whereever you want to start crawling from - -api.get_suggested_users_by_id_crawler(startingId=userId) diff --git a/examples/video_example.py b/examples/video_example.py new file mode 100644 index 00000000..6ebb17c7 --- /dev/null +++ b/examples/video_example.py @@ -0,0 +1,12 @@ +from TikTokApi import TikTokApi + +verify_fp = "verify_xxx" +api = TikTokApi(custom_verify_fp=verify_fp) + +video = api.video(id="7041997751718137094") + +# Bytes of the TikTok video +video_data = video.bytes() + +with open("out.mp4", "wb") as out_file: + out_file.write(video_data) From 3a137969f4e6a134151b6121d3cf44e8bd2842b5 Mon Sep 17 00:00:00 2001 From: davidteather Date: Sun, 23 Jan 2022 19:18:25 -0600 Subject: [PATCH 05/17] Fix package-test --- .github/workflows/package-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/package-test.yml b/.github/workflows/package-test.yml index 43d9d179..27ff8ca3 100644 --- a/.github/workflows/package-test.yml +++ b/.github/workflows/package-test.yml @@ -7,7 +7,7 @@ on: branches: - master - nightly - - 'releases/*' + - "releases/*" jobs: Unit-Tests: @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: os: [macos-latest] - python-version: [3.7, 3.10] + python-version: [3.7, "3.10"] steps: - uses: actions/checkout@v2 - uses: microsoft/playwright-github-action@v1 From ca2eabfdc35a74e4132b1d801d4f07e986e2daa4 Mon Sep 17 00:00:00 2001 From: davidteather Date: Sun, 23 Jan 2022 21:06:42 -0600 Subject: [PATCH 06/17] Bump version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index ab0138d6..fa0eeeb0 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setuptools.setup( name="TikTokApi", packages=setuptools.find_packages(), - version="4.1.0", + version="5.0.0", license="MIT", description="The Unofficial TikTok API Wrapper in Python 3.", author="David Teather", From a2274a4216eb7b40752c58a3c0beb53953b776f0 Mon Sep 17 00:00:00 2001 From: davidteather Date: Mon, 24 Jan 2022 15:11:29 -0600 Subject: [PATCH 07/17] simplify info_full --- TikTokApi/api/sound.py | 6 +++--- TikTokApi/api/user.py | 7 ++++--- tests/test_sound.py | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/TikTokApi/api/sound.py b/TikTokApi/api/sound.py index b225e709..b1dd0b3f 100644 --- a/TikTokApi/api/sound.py +++ b/TikTokApi/api/sound.py @@ -36,7 +36,7 @@ def __init__(self, id: Optional[str] = None, data: Optional[str] = None): def info(self, use_html=False, **kwargs) -> dict: if use_html: - return self.info_full(**kwargs)["props"]["pageProps"]["musicInfo"] + return self.info_full(**kwargs)["musicInfo"] ( region, @@ -53,7 +53,7 @@ def info(self, use_html=False, **kwargs) -> dict: if res.get("statusCode", 200) == 10203: raise TikTokNotFoundError() - return res["musicInfo"] + return res["musicInfo"]["music"] def info_full(self, **kwargs) -> dict: r = requests.get( @@ -70,7 +70,7 @@ def info_full(self, **kwargs) -> dict: ) data = extract_tag_contents(r.text) - return json.loads(data) + return json.loads(data)["props"]["pageProps"]["musicInfo"] def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]: """Returns a dictionary listing TikToks with a specific sound. diff --git a/TikTokApi/api/user.py b/TikTokApi/api/user.py index 4148c611..28d73879 100644 --- a/TikTokApi/api/user.py +++ b/TikTokApi/api/user.py @@ -47,7 +47,7 @@ def __init__( def info(self, **kwargs): # TODO: Might throw a key error with the HTML - return self.info_full(**kwargs)["props"]["pageProps"]["userInfo"]["user"] + return self.info_full(**kwargs)["user"] def info_full(self, **kwargs) -> dict: """Gets all data associated with the user.""" @@ -76,12 +76,13 @@ def info_full(self, **kwargs) -> dict: data = extract_tag_contents(r.text) user = json.loads(data) - if user["props"]["pageProps"]["statusCode"] == 404: + user_props = user["props"]["pageProps"] + if user_props["statusCode"] == 404: raise TikTokNotFoundError( "TikTok user with username {} does not exist".format(self.username) ) - return user + return user_props["userInfo"] def videos(self, count=30, cursor=0, **kwargs) -> Generator[Video, None, None]: """Returns an array of dictionaries representing TikToks for a user. diff --git a/tests/test_sound.py b/tests/test_sound.py index 0a85798e..fa809f8d 100644 --- a/tests/test_sound.py +++ b/tests/test_sound.py @@ -19,4 +19,4 @@ def test_sound_videos(): def test_sound_info(): sound = api.sound(id=song_id) data = sound.info() - assert data["music"]["id"] == song_id + assert data["id"] == song_id From 5bb0a642fad6749d5b4d0d2ad7a1b584997a75f6 Mon Sep 17 00:00:00 2001 From: davidteather Date: Mon, 24 Jan 2022 23:52:36 -0600 Subject: [PATCH 08/17] Logging Improvements Logging changes from #804 by @zokalo Co-Authored-By: Dmitriy <16061619+zokalo@users.noreply.github.com> --- TikTokApi/api/hashtag.py | 6 +++-- TikTokApi/api/search.py | 8 +++++-- TikTokApi/api/sound.py | 4 +++- TikTokApi/api/trending.py | 4 +++- TikTokApi/api/user.py | 12 ++++++---- TikTokApi/api/video.py | 2 +- TikTokApi/browser_utilities/browser.py | 6 +++-- TikTokApi/tiktok.py | 33 +++++++++++++------------- TikTokApi/utilities.py | 2 +- examples/sound_example.py | 3 +-- 10 files changed, 47 insertions(+), 33 deletions(-) diff --git a/TikTokApi/api/hashtag.py b/TikTokApi/api/hashtag.py index cbf605a1..3cb56e1b 100644 --- a/TikTokApi/api/hashtag.py +++ b/TikTokApi/api/hashtag.py @@ -103,7 +103,9 @@ def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]: yield self.parent.video(data=result) if not res.get("hasMore", False): - logging.info("TikTok isn't sending more TikToks beyond this point.") + self.parent.logger.info( + "TikTok isn't sending more TikToks beyond this point." + ) return cursor = int(res["cursor"]) @@ -117,7 +119,7 @@ def __extract_from_data(self): self.name = data["title"] if None in (self.name, self.id): - logging.error( + Hashtag.parent.logger.error( f"Failed to create Hashtag with data: {data}\nwhich has keys {data.keys()}" ) diff --git a/TikTokApi/api/search.py b/TikTokApi/api/search.py index 4d1668aa..fac59c9d 100644 --- a/TikTokApi/api/search.py +++ b/TikTokApi/api/search.py @@ -106,7 +106,9 @@ def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> list: yield Hashtag(data=x["challenge"]) if int(data["offset"]) <= offset: - logging.info("TikTok is not sending videos beyond this point.") + Search.parent.logger.info( + "TikTok is not sending videos beyond this point." + ) return offset = int(data["offset"]) @@ -158,7 +160,9 @@ def users_alternate(search_term, count=28, offset=0, **kwargs) -> list: yield User(data=result) if data.get("has_more", 0) == 0: - logging.info("TikTok is not sending videos beyond this point.") + Search.parent.logger.info( + "TikTok is not sending videos beyond this point." + ) return cursor = int(data.get("cursor")) diff --git a/TikTokApi/api/sound.py b/TikTokApi/api/sound.py index b1dd0b3f..cdfb5443 100644 --- a/TikTokApi/api/sound.py +++ b/TikTokApi/api/sound.py @@ -107,7 +107,9 @@ def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]: yield self.parent.video(data=result) if not res.get("hasMore", False): - logging.info("TikTok isn't sending more TikToks beyond this point.") + self.parent.logger.info( + "TikTok isn't sending more TikToks beyond this point." + ) return cursor = int(res["cursor"]) diff --git a/TikTokApi/api/trending.py b/TikTokApi/api/trending.py index aec27468..96527440 100644 --- a/TikTokApi/api/trending.py +++ b/TikTokApi/api/trending.py @@ -66,7 +66,9 @@ def videos(count=30, **kwargs) -> Generator[Video, None, None]: amount_yielded += len(res.get("itemList", [])) if not res.get("hasMore", False) and not first: - logging.info("TikTok isn't sending more TikToks beyond this point.") + Trending.parent.logger.info( + "TikTok isn't sending more TikToks beyond this point." + ) return first = False diff --git a/TikTokApi/api/user.py b/TikTokApi/api/user.py index 28d73879..698cc553 100644 --- a/TikTokApi/api/user.py +++ b/TikTokApi/api/user.py @@ -132,7 +132,9 @@ def videos(self, count=30, cursor=0, **kwargs) -> Generator[Video, None, None]: yield self.parent.video(data=video) if not res.get("hasMore", False) and not first: - logging.info("TikTok isn't sending more TikToks beyond this point.") + User.parent.logger.info( + "TikTok isn't sending more TikToks beyond this point." + ) return cursor = res["cursor"] @@ -187,7 +189,7 @@ def liked( res = self.parent.get_data(path, **kwargs) if "itemList" not in res.keys(): - logging.error("User's likes are most likely private") + User.parent.logger.error("User's likes are most likely private") return videos = res.get("itemList", []) @@ -197,7 +199,9 @@ def liked( yield self.parent.video(data=video) if not res.get("hasMore", False) and not first: - logging.info("TikTok isn't sending more TikToks beyond this point.") + User.parent.logger.info( + "TikTok isn't sending more TikToks beyond this point." + ) return cursor = res["cursor"] @@ -219,7 +223,7 @@ def __extract_from_data(self): ) if None in (self.username, self.user_id, self.sec_uid): - logging.error( + User.parent.logger.error( f"Failed to create User with data: {data}\nwhich has keys {data.keys()}" ) diff --git a/TikTokApi/api/video.py b/TikTokApi/api/video.py index e2151ffa..26d8afdc 100644 --- a/TikTokApi/api/video.py +++ b/TikTokApi/api/video.py @@ -103,7 +103,7 @@ def __extract_from_data(self) -> None: ] if self.id is None: - logging.error( + Video.parent.logger.error( f"Failed to create Video with data: {data}\nwhich has keys {data.keys()}" ) diff --git a/TikTokApi/browser_utilities/browser.py b/TikTokApi/browser_utilities/browser.py index 248982b5..704d0362 100644 --- a/TikTokApi/browser_utilities/browser.py +++ b/TikTokApi/browser_utilities/browser.py @@ -12,11 +12,13 @@ # Import Detection From Stealth +from ..utilities import LOGGER_NAME from .get_acrawler import get_acrawler, get_tt_params_script from playwright.sync_api import sync_playwright playwright = None +logger = logging.getLogger(LOGGER_NAME) def get_playwright(): global playwright @@ -80,7 +82,7 @@ def __init__( args=self.args, **self.options ) except Exception as e: - logging.critical(e) + logger.critical("Webkit launch failed", exc_info=True) raise e context = self.create_context(set_useragent=True) @@ -244,7 +246,7 @@ def clean_up(self): try: self.browser.close() except Exception: - logging.info("cleanup failed") + logger.exception("cleanup failed") # playwright.stop() def find_redirect(self, url): diff --git a/TikTokApi/tiktok.py b/TikTokApi/tiktok.py index d279ad67..1ddfb1a3 100644 --- a/TikTokApi/tiktok.py +++ b/TikTokApi/tiktok.py @@ -4,6 +4,7 @@ import random import string import time +from typing import ClassVar from urllib.parse import quote, urlencode import requests @@ -11,7 +12,7 @@ from playwright.sync_api import sync_playwright from .exceptions import * -from .utilities import update_messager +from .utilities import LOGGER_NAME, update_messager from .browser_utilities.browser import browser os.environ["no_proxy"] = "127.0.0.1,localhost" @@ -22,6 +23,7 @@ class TikTokApi: _instance = None + logger: ClassVar[logging.Logger] = logging.getLogger(LOGGER_NAME) @staticmethod def __new__(cls, logging_level=logging.WARNING, *args, **kwargs): @@ -112,13 +114,13 @@ def _initialize(self, logging_level=logging.WARNING, **kwargs): trending.Trending.parent = self self.trending = trending.Trending - logging.basicConfig(level=logging_level) + self.logger.setLevel(level=logging_level) # Some Instance Vars self.executablePath = kwargs.get("executablePath", None) if kwargs.get("custom_did") != None: - raise Exception("Please use custom_device_id instead of custom_device_id") + 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.proxy = kwargs.get("proxy", None) @@ -150,11 +152,10 @@ def _initialize(self, logging_level=logging.WARNING, **kwargs): self.region = self.browser.region self.language = self.browser.language except Exception as e: - logging.exception(e) - logging.warning( - "An error ocurred while opening your browser but it was ignored." + self.logger.exception( + "An error occurred while opening your browser, but it was ignored\n", + "Are you sure you ran python -m playwright install?", ) - logging.warning("Are you sure you ran python -m playwright install") self.timezone_name = "" self.browser_language = "" @@ -257,7 +258,7 @@ def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict: "x-tt-params": tt_params, } - logging.info(f"GET: {url}\n\theaders: {headers}") + self.logger.info(f"GET: %s\n\theaders: %s", url, headers) r = requests.get( url, headers=headers, @@ -272,11 +273,10 @@ def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict: json.get("type") == "verify" or json.get("verifyConfig", {}).get("type", "") == "verify" ): - logging.error( - "Tiktok wants to display a catcha. Response is:\n" + r.text - ) - logging.info( - f"Request failed with\nurl:{url}\nheaders:{headers}\ncookies:{self.get_cookies(**kwargs)}" + self.logger.error( + "Tiktok wants to display a captcha.\nResponse:\n%s\nCookies:\n%s", + r.text, + self.get_cookies(**kwargs), ) raise TikTokCaptchaError() @@ -322,7 +322,7 @@ def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict: "undefined": "MEDIA_ERROR", } statusCode = json.get("statusCode", 0) - logging.info(f"TikTok Returned: {json}") + self.logger.info(f"TikTok Returned: %s", json) if statusCode == 10201: # Invalid Entity raise TikTokNotFoundError( @@ -341,14 +341,13 @@ def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict: return r.json() except ValueError as e: text = r.text - logging.error("TikTok response: " + text) + self.logger.info("TikTok response: %s", text) if len(text) == 0: raise EmptyResponseError( "Empty response from Tiktok to " + url ) from None else: - logging.error("Converting response to JSON failed") - logging.error(e) + self.logger.exception("Converting response to JSON failed") raise JSONDecodeFailure() from e def clean_up(self): diff --git a/TikTokApi/utilities.py b/TikTokApi/utilities.py index 337b94f9..61ee915b 100644 --- a/TikTokApi/utilities.py +++ b/TikTokApi/utilities.py @@ -1,7 +1,7 @@ import subprocess import sys -import pkg_resources +LOGGER_NAME: str = "TikTokApi" def update_messager(): if not check("TikTokApi"): diff --git a/examples/sound_example.py b/examples/sound_example.py index db51bc18..b0c8dc75 100644 --- a/examples/sound_example.py +++ b/examples/sound_example.py @@ -3,8 +3,7 @@ verify_fp = "verify_xxx" api = TikTokApi(custom_verify_fp=verify_fp) -sound = api.sound(id='7016547803243022337') +sound = api.sound(id="7016547803243022337") for video in sound.videos(): print(video.id) - From c168a7852412472cf0c3d089117fb4f4285d4df2 Mon Sep 17 00:00:00 2001 From: davidteather Date: Mon, 24 Jan 2022 23:57:15 -0600 Subject: [PATCH 09/17] I forgot to save one file on the previous commit --- TikTokApi/api/sound.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TikTokApi/api/sound.py b/TikTokApi/api/sound.py index cdfb5443..2a96f567 100644 --- a/TikTokApi/api/sound.py +++ b/TikTokApi/api/sound.py @@ -126,7 +126,7 @@ def __extract_from_data(self): self.author = self.parent.user(username=data["authorName"]) if self.id is None: - logging.error( + Sound.parent.logger.error( f"Failed to create Sound with data: {data}\nwhich has keys {data.keys()}" ) From ad52e51db928a3d7ddd667d4f4dc1338f1784745 Mon Sep 17 00:00:00 2001 From: davidteather Date: Tue, 25 Jan 2022 20:36:46 -0600 Subject: [PATCH 10/17] Improve docs --- CITATION.cff | 10 +- README.md | 122 +- TikTokApi/api/__init__.py | 5 + TikTokApi/api/hashtag.py | 41 +- TikTokApi/api/search.py | 87 +- TikTokApi/api/sound.py | 56 +- TikTokApi/api/trending.py | 11 +- TikTokApi/api/user.py | 84 +- TikTokApi/api/video.py | 50 +- TikTokApi/browser_utilities/browser.py | 21 +- .../browser_utilities/browser_interface.py | 2 +- TikTokApi/browser_utilities/get_acrawler.py | 4 +- TikTokApi/browser_utilities/stealth.py | 502 --- TikTokApi/tiktok.py | 52 +- TikTokApi/utilities.py | 1 + docs/TikTokApi.html | 420 +++ docs/TikTokApi/api.html | 250 ++ docs/TikTokApi/api/hashtag.html | 838 +++++ docs/TikTokApi/api/search.html | 1026 ++++++ docs/TikTokApi/api/sound.html | 926 +++++ docs/TikTokApi/api/trending.html | 495 +++ docs/TikTokApi/api/user.html | 1276 +++++++ docs/TikTokApi/api/video.html | 826 +++++ docs/TikTokApi/browser_utilities.html | 239 ++ docs/TikTokApi/browser_utilities/browser.html | 1062 ++++++ .../browser_utilities/browser_interface.html | 348 ++ .../browser_utilities/get_acrawler.html | 242 ++ docs/TikTokApi/browser_utilities/stealth.html | 1455 ++++++++ docs/TikTokApi/exceptions.html | 654 ++++ docs/TikTokApi/helpers.html | 348 ++ docs/TikTokApi/tiktok.html | 3130 +++++++++++++++++ docs/TikTokApi/utilities.html | 398 +++ docs/index.html | 7 + docs/search.js | 46 + setup.py | 2 +- 35 files changed, 14371 insertions(+), 665 deletions(-) delete mode 100644 TikTokApi/browser_utilities/stealth.py create mode 100644 docs/TikTokApi.html create mode 100644 docs/TikTokApi/api.html create mode 100644 docs/TikTokApi/api/hashtag.html create mode 100644 docs/TikTokApi/api/search.html create mode 100644 docs/TikTokApi/api/sound.html create mode 100644 docs/TikTokApi/api/trending.html create mode 100644 docs/TikTokApi/api/user.html create mode 100644 docs/TikTokApi/api/video.html create mode 100644 docs/TikTokApi/browser_utilities.html create mode 100644 docs/TikTokApi/browser_utilities/browser.html create mode 100644 docs/TikTokApi/browser_utilities/browser_interface.html create mode 100644 docs/TikTokApi/browser_utilities/get_acrawler.html create mode 100644 docs/TikTokApi/browser_utilities/stealth.html create mode 100644 docs/TikTokApi/exceptions.html create mode 100644 docs/TikTokApi/helpers.html create mode 100644 docs/TikTokApi/tiktok.html create mode 100644 docs/TikTokApi/utilities.html create mode 100644 docs/index.html create mode 100644 docs/search.js diff --git a/CITATION.cff b/CITATION.cff index e21b1c8f..554b47bf 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,9 +1,9 @@ cff-version: 1.2.0 authors: -- family-names: "Teather" - given-names: "David" - orcid: "https://orcid.org/0000-0002-9467-4676" + - family-names: "Teather" + given-names: "David" + orcid: "https://orcid.org/0000-0002-9467-4676" title: "TikTokAPI" url: "https://github.com/davidteather/tiktok-api" -version: 4.1.0 -date-released: 2021-12-11 \ No newline at end of file +version: 5.0.0 +date-released: 2022-1-31 diff --git a/README.md b/README.md index d949122a..e4b2fa7d 100644 --- a/README.md +++ b/README.md @@ -8,46 +8,46 @@ 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](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) +[![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) - [Getting Started](#getting-started) + - [How to Support The Project](#how-to-support-the-project) - [Installing](#installing) - [Common Issues](#common-issues) - [Quick Start Guide](#quick-start-guide) - [Examples](https://github.com/davidteather/TikTok-Api/tree/master/examples) -- [Documentation](#documentation) -- [Built With](#built-with) -- [Authors](#authors) -- [License](#license) +[**Upgrading from V4 to V5**](#upgrading-from-v4-to-v5) + +## Documentation + +You can find the full documentation [here](https://davidteather.github.io/TikTok-Api/docs/TikTokApi.html), the [TikTokApi Class](https://davidteather.github.io/TikTok-Api/docs/TikTokApi/tiktok.html) is where you'll probably spend most of your time. ## Getting Started To get started using this api follow the instructions below. -#### How to support the project -* Feel free to sponsor me on GitHub -* Feel free to tip the project using the brave browser +### How to Support The Project +* Star the repo 😎 +* Consider [sponsoring](https://github.com/sponsors/davidteather) me on GitHub +* Send me an email or a [LinkedIn](https://www.linkedin.com/in/davidteather/) message telling me what you're using the API for, I really like hearing what people are using it for. * Submit PRs for issues :) ### Installing -If you run into an issue please check the closed issues on the github. You're most likely not the first person to experience this issue. If nothing works feel free to open an issue. +If you run into an issue please check the closed issues on the github, although feel free to re-open a new issue if you find an issue that's been closed for a few months. The codebase can and does run into similar issues as it has before, because TikTok changes things up. ```sh pip install TikTokApi python -m playwright install ``` -If you would prefer a video walk through of setting up this package I created a [YouTube video](https://www.youtube.com/watch?v=zwLmLfVI-VQ) just for that. - - - -If you're on MacOS you may need to install [XCode Developer Tools](https://webkit.org/build-tools/) +If you would prefer a video walk through of setting up this package I created a currently semi-outdated (TODO: new one for v5 coming soon) [YouTube video](https://www.youtube.com/watch?v=zwLmLfVI-VQ) just for that. #### Docker Installation -Clone this repository onto a local machine then run the following commands. +Clone this repository onto a local machine (or just the Dockerfile since it installs TikTokApi from pip) then run the following commands. ```sh docker pull mcr.microsoft.com/playwright:focal @@ -61,47 +61,93 @@ docker run -v TikTokApi --rm tiktokapi:latest python3 your_script.py Please don't open an issue if you're experiencing one of these just comment if the provided solution do not work for you. -* **Browser Has no Attribute** - make sure you ran `python3 -m playwright install`, if your error persists try the [playwright](https://github.com/microsoft/playwright-python) quickstart guide and diagnose issues from there. +* **Browser Has no Attribute** - make sure you ran `python3 -m playwright install`, if your error persists try the [playwright-python](https://github.com/microsoft/playwright-python) quickstart guide and diagnose issues from there. ## Quick Start Guide -Here's a quick bit of code to get the most recent trending on TikTok. There's more examples in the examples directory. +Here's a quick bit of code to get the most recent trending videos on TikTok. There's more examples in the [examples](https://github.com/davidteather/TikTok-Api/tree/master/examples) directory. ```py from TikTokApi import TikTokApi -api = TikTokApi.get_instance() -results = 10 -# Since TikTok changed their API you need to use the custom_verify_fp option. -# In your web browser you will need to go to TikTok, Log in and get the s_v_web_id value. -trending = api.by_trending(count=results, custom_verify_fp="") +# 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=zwLmLfVI-VQ for a visual +# TODO: Update link +api = TikTokApi(custom_verify_fp="") -for tiktok in trending: - # Prints the id of the tiktok - print(tiktok['id']) - -print(len(trending)) +for trending_video in api.trending.videos(count=50): + # Prints the author's username of the trending video. + print(trending_video.author.username) ``` -To run the example scripts from the repository root, make sure you use the -module form of python the interpreter - +To run the example scripts from the repository root, make sure you use the `-m` option on python. ```sh python -m examples.get_trending ``` -[Here's](https://gist.github.com/davidteather/7c30780bbc30772ba11ec9e0b909e99d) an example of what a TikTok dictionary looks like. +You can access the dictionary type of an object using `.as_dict`. On a video this may look like +[this](https://gist.github.com/davidteather/7c30780bbc30772ba11ec9e0b909e99d), although TikTok changes their structure from time to time so it's worth investigating the structure of the dictionary when you use this package. -## Documentation +## Upgrading from V4 to V5 + +All changes will be noted on [#803](https://github.com/davidteather/TikTok-Api/pull/803) if you want more information. + +### Motivation -You can find the documentation [here](https://davidteather.github.io/TikTok-Api/docs/TikTokApi.html) (you'll likely just need the TikTokApi section of the docs), I will be making this documentation more complete overtime as it's not super great right now, but better than just having it in the readme! +This package has been difficult to maintain due to it's structure, difficult to work with since the user of the package must write parsing methods to extract information from dictionaries, more memory intensive than it needs to be (although this can be further improved), and in general just difficult to work with for new users. -## Authors +As a result, I've decided to at least attempt to remedy some of these issues, the biggest changes are that +1. The package has shifted to using classes for different TikTok objects resulting in an easier, higher-level programming experience. +2. All methods that used to return a list of objects have been switched to using generators, to hopefully decrease memory utilization for most users. -* **David Teather** - *Initial work* - [davidteather](https://github.com/davidteather) -See also the list of [contributors](https://github.com/davidteather/TikTok-Api/contributors) who participated in this project. +### Upgrading Examples -## License -This project is licensed under the MIT License +#### Accessing Dictionary on Objects (similar to V4) + +TODO: Make video upgrading from V4-V5? + +You'll probably need to use this beyond just for legacy support, since not all attributes are parsed out and attached +to the different objects. + +You may want to use this as a workaround for legacy applications while you upgrade the rest of the app. I'd suggest that you do eventually upgrade to using the higher-level approach fully. +```py +user = api.user(username='therock') +user.as_dict # -> dict of the user_object +for video in user.videos(): + video.as_dict # -> dict of TikTok's video object as found when requesting the videos endpoint +``` + +Here's a few more examples that help illustrate the differences in the flow of the usage of the package with V5. + +```py +# V4 +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 +``` + +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. +```py +# V4 +trending_videos = api.by_trending() +for video in trending_videos: + # The dictionary responses are also different depending on what endpoint you got them from + # So, it's usually more painful than this to deal with + trending_user = api.get_user(id=video['author']['id'], secUid=video['author']['secUid']) + + +# V5 +# This is more complicated than above, but it illustrates the simplified approach +for trending_video in api.trending.videos(): + user_stats = trending_video.author.info_full['stats'] + if user_stats['followerCount'] >= 10000: + # maybe save the user in a database +``` diff --git a/TikTokApi/api/__init__.py b/TikTokApi/api/__init__.py index e69de29b..48ff818a 100644 --- a/TikTokApi/api/__init__.py +++ b/TikTokApi/api/__init__.py @@ -0,0 +1,5 @@ +""" +This module contains classes that all represent different types of data sent back by the TikTok servers. + +The files within in module correspond to what type of object is described and all have different methods associated with them. +""" diff --git a/TikTokApi/api/hashtag.py b/TikTokApi/api/hashtag.py index 3cb56e1b..75dc7d17 100644 --- a/TikTokApi/api/hashtag.py +++ b/TikTokApi/api/hashtag.py @@ -12,11 +12,23 @@ class Hashtag: + """ + A TikTok Hashtag/Challenge. + + Example Usage + ```py + hashtag = api.hashtag(name='funny') + ``` + """ + parent: ClassVar[TikTokApi] id: str + """The ID of the hashtag""" name: str + """The name of the hashtag (omiting the #)""" as_dict: dict + """The raw data associated with this hashtag.""" def __init__( self, @@ -24,6 +36,9 @@ def __init__( id: Optional[str] = None, data: Optional[str] = None, ): + """ + You must provide the name or id of the hashtag. + """ self.name = name self.id = id @@ -32,15 +47,19 @@ def __init__( self.__extract_from_data() def info(self, **kwargs) -> dict: + """ + Returns TikTok's dictionary representation of the hashtag object. + """ return self.info_full(**kwargs)["challengeInfo"]["challenge"] def info_full(self, **kwargs) -> dict: - """Returns a hashtag object. - - ##### Parameters - * hashtag: The hashtag to search by + """ + Returns all information sent by TikTok related to this hashtag. - Without the # symbol + Example Usage + ```py + hashtag_data = api.hashtag(name='funny').info_full() + ``` """ ( region, @@ -69,9 +88,15 @@ def info_full(self, **kwargs) -> dict: def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]: """Returns a dictionary listing TikToks with a specific hashtag. - ##### Parameters - * count: The number of posts to return - Note: seems to only support up to ~2,000 + - Parameters: + - count (int): The amount of videos you want returned. + - cursor (int): The unix epoch to get videos since. TODO: Check this is right + + Example Usage + ```py + for video in api.hashtag(name='funny').videos(): + # do something + ``` """ ( region, diff --git a/TikTokApi/api/search.py b/TikTokApi/api/search.py index fac59c9d..abf52a7b 100644 --- a/TikTokApi/api/search.py +++ b/TikTokApi/api/search.py @@ -11,49 +11,62 @@ if TYPE_CHECKING: from ..tiktok import TikTokApi -import logging import requests class Search: + """Contains static methods about searching.""" + parent: TikTokApi @staticmethod def users(search_term, count=28, **kwargs) -> Generator[User, None, None]: - """Returns a list of users that match the search_term + """ + Searches for users. - ##### Parameters - * search_term: The string to search for users by - This string is the term you want to search for users by. + - Parameters: + - search_term (str): The phrase you want to search for. + - count (int): The amount of videos you want returned. - * count: The number of users to return - Note: maximum is around 28 for this type of endpoint. + Example Usage + ```py + for user in api.search.users('therock'): + # do something + ``` """ return Search.discover_type(search_term, prefix="user", count=count, **kwargs) @staticmethod def sounds(search_term, count=28, **kwargs) -> Generator[Sound, None, None]: - """Returns a list of sounds that match the search_term + """ + Searches for sounds. - ##### Parameters - * search_term: The string to search for sounds by - This string is the term you want to search for sounds by. + - Parameters: + - search_term (str): The phrase you want to search for. + - count (int): The amount of videos you want returned. - * count: The number of sounds to return - Note: maximum is around 28 for this type of endpoint. + Example Usage + ```py + for user in api.search.sounds('funny'): + # do something + ``` """ return Search.discover_type(search_term, prefix="music", count=count, **kwargs) @staticmethod def hashtags(search_term, count=28, **kwargs) -> Generator[Hashtag, None, None]: - """Returns a list of hashtags that match the search_term + """ + Searches for hashtags/challenges. - ##### Parameters - * search_term: The string to search for hashtags by - This string is the term you want to search for hashtags by. + - Parameters: + - search_term (str): The phrase you want to search for. + - count (int): The amount of videos you want returned. - * count: The number of hashtags to return - Note: maximum is around 28 for this type of endpoint. + Example Usage + ```py + for user in api.search.hashtags('funny'): + # do something + ``` """ return Search.discover_type( search_term, prefix="challenge", count=count, **kwargs @@ -61,12 +74,22 @@ def hashtags(search_term, count=28, **kwargs) -> Generator[Hashtag, None, None]: @staticmethod def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> list: - """Returns a list of whatever the prefix type you pass in - ##### Parameters - * search_term: The string to search by - * prefix: The prefix of what to search for - * count: The number search results to return - Note: maximum is around 28 for this type of endpoint. + """ + Searches for a specific type of object. + You should instead use the users/sounds/hashtags as they all use data + from this function. + + - Parameters: + - search_term (str): The phrase you want to search for. + - prefix (str): either user|music|challenge + - count (int): The amount of videos you want returned. + + Example Usage + ```py + for user in api.search.discover_type('therock', 'user'): + # do something + ``` + """ # TODO: Investigate if this is actually working as expected. Doesn't seem to be ( @@ -115,12 +138,18 @@ def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> list: @staticmethod def users_alternate(search_term, count=28, offset=0, **kwargs) -> list: - """Returns a list of whatever the prefix type you pass in + """ + Searches for users using an alternate endpoint than Search.users - ##### Parameters - * search_term: The string to search by + - Parameters: + - search_term (str): The phrase you want to search for. + - count (int): The amount of videos you want returned. - * count: The number search results to return + Example Usage + ```py + for user in api.search.users_alternate('therock'): + # do something + ``` """ ( region, diff --git a/TikTokApi/api/sound.py b/TikTokApi/api/sound.py index 2a96f567..f2d66958 100644 --- a/TikTokApi/api/sound.py +++ b/TikTokApi/api/sound.py @@ -2,7 +2,6 @@ from os import path import requests -import logging import json from urllib.parse import quote, urlencode @@ -19,13 +18,28 @@ class Sound: + """ + A TikTok Sound/Music/Song. + + Example Usage + ```py + song = api.song(id='7016547803243022337') + ``` + """ + parent: ClassVar[TikTokApi] id: str + """TikTok's ID for the sound""" title: Optional[str] + """The title of the song.""" author: Optional[User] + """The author of the song (if it exists)""" def __init__(self, id: Optional[str] = None, data: Optional[str] = None): + """ + You must provide the id of the sound or it will not work. + """ if data is not None: self.as_dict = data self.__extract_from_data() @@ -35,6 +49,20 @@ def __init__(self, id: Optional[str] = None, data: Optional[str] = None): self.id = id def info(self, use_html=False, **kwargs) -> dict: + """ + Returns a dictionary of TikTok's Sound/Music object. + + - Parameters: + - use_html (bool): If you want to perform an HTML request or not. + Defaults to False to use an API call, which shouldn't get detected + as often as an HTML request. + + + Example Usage + ```py + sound_data = api.sound(id='7016547803243022337').info() + ``` + """ if use_html: return self.info_full(**kwargs)["musicInfo"] @@ -56,6 +84,17 @@ def info(self, use_html=False, **kwargs) -> dict: return res["musicInfo"]["music"] def info_full(self, **kwargs) -> dict: + """ + Returns all the data associated with a TikTok Sound. + + This makes an API request, there is no HTML request option, as such + with Sound.info() + + Example Usage + ```py + sound_data = api.sound(id='7016547803243022337').info_full() + ``` + """ r = requests.get( "https://www.tiktok.com/music/-{}".format(self.id), headers={ @@ -65,7 +104,7 @@ def info_full(self, **kwargs) -> dict: "User-Agent": self.parent.user_agent, }, proxies=self.parent._format_proxy(kwargs.get("proxy", None)), - cookies=self.parent.get_cookies(**kwargs), + cookies=self.parent._get_cookies(**kwargs), **self.parent.requests_extra_kwargs, ) @@ -73,9 +112,18 @@ def info_full(self, **kwargs) -> dict: return json.loads(data)["props"]["pageProps"]["musicInfo"] def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]: - """Returns a dictionary listing TikToks with a specific sound. + """ + Returns Video objects of videos created with this sound. + + - Parameters: + - count (int): The amount of videos you want returned. + - cursor (int): The unix epoch to get videos since. TODO: Check this is right - Note: seems to only support up to ~2,000 + Example Usage + ```py + for video in api.sound(id='7016547803243022337').videos(): + # do something + ``` """ ( region, diff --git a/TikTokApi/api/trending.py b/TikTokApi/api/trending.py index 96527440..b0a21e1c 100644 --- a/TikTokApi/api/trending.py +++ b/TikTokApi/api/trending.py @@ -13,18 +13,17 @@ class Trending: + """Contains static methods related to trending.""" + parent: TikTokApi @staticmethod def videos(count=30, **kwargs) -> Generator[Video, None, None]: """ - Gets trending TikToks - - ##### Parameters - * count: The amount of TikToks you want returned, optional + Returns Videos that are trending on TikTok. - Note: TikTok seems to only support at MOST ~2000 TikToks - from a single endpoint. + - Parameters: + - count (int): The amount of videos you want returned. """ ( diff --git a/TikTokApi/api/user.py b/TikTokApi/api/user.py index 698cc553..cafa8f42 100644 --- a/TikTokApi/api/user.py +++ b/TikTokApi/api/user.py @@ -1,7 +1,6 @@ from __future__ import annotations import json -import logging import requests from urllib.parse import quote, urlencode @@ -17,21 +16,30 @@ class User: - """A TikTok User class + """ + A TikTok User. + + Example Usage + ```py + user = api.user(username='therock') + # or + user_id = '5831967' + sec_uid = 'MS4wLjABAAAA-VASjiXTh7wDDyXvjk10VFhMWUAoxr8bgfO1kAL1-9s' + user = api.user(user_id=user_id, sec_uid=sec_uid) + ``` - Attributes - user_id: The TikTok user's ID. - sec_uid: The TikTok user's sec_uid. - username: The TikTok user's username. - as_dict: The dictionary provided to create the class. """ parent: ClassVar[TikTokApi] user_id: str + """The user ID of the user.""" sec_uid: str + """The sec UID of the user.""" username: str + """The username of the user.""" as_dict: dict + """The raw data associated with this user.""" def __init__( self, @@ -40,17 +48,36 @@ def __init__( sec_uid: Optional[str] = None, data: Optional[str] = None, ): + """ + You must provide the username or (user_id and sec_uid) otherwise this + will not function correctly. + """ self.__update_id_sec_uid_username(user_id, sec_uid, username) if data is not None: self.as_dict = data self.__extract_from_data() def info(self, **kwargs): - # TODO: Might throw a key error with the HTML + """ + Returns a dictionary of TikTok's User object + + Example Usage + ```py + user_data = api.user(username='therock').info() + ``` + """ return self.info_full(**kwargs)["user"] def info_full(self, **kwargs) -> dict: - """Gets all data associated with the user.""" + """ + Returns a dictionary of information associated with this User. + Includes statistics about this user. + + Example Usage + ```py + user_data = api.user(username='therock').info_full() + ``` + """ # TODO: Find the one using only user_id & sec_uid if not self.username: @@ -69,7 +96,7 @@ def info_full(self, **kwargs) -> dict: "User-Agent": self.parent.user_agent, }, proxies=User.parent._format_proxy(kwargs.get("proxy", None)), - cookies=User.parent.get_cookies(**kwargs), + cookies=User.parent._get_cookies(**kwargs), **User.parent.requests_extra_kwargs, ) @@ -85,12 +112,19 @@ def info_full(self, **kwargs) -> dict: return user_props["userInfo"] def videos(self, count=30, cursor=0, **kwargs) -> Generator[Video, None, None]: - """Returns an array of dictionaries representing TikToks for a user. - - ##### Parameters - * count: The number of posts to return - - Note: seems to only support up to ~2,000 + """ + Returns a Generator yielding Video objects. + + - Parameters: + - count (int): The amount of videos you want returned. + - cursor (int): The unix epoch to get videos since. TODO: Check this is right + + Example Usage + ```py + user = api.user(username='therock') + for video in user.videos(count=100): + print(video.id) + ``` """ ( region, @@ -143,16 +177,20 @@ def videos(self, count=30, cursor=0, **kwargs) -> Generator[Video, None, None]: def liked( self, count: int = 30, cursor: int = 0, **kwargs ) -> Generator[Video, None, None]: - """Returns a dictionary listing TikToks that a given a user has liked. - Note: The user's likes must be public + """ + Returns a dictionary listing TikToks that a given a user has liked. - ##### Parameters - * count: The number of posts to return + **Note**: The user's likes must be **public** (which is not the default option) - Note: seems to only support up to ~2,000 - * cursor: The offset of a page + - Parameters: + - count (int): The amount of videos you want returned. + - cursor (int): The unix epoch to get videos since. TODO: Check this is right - The offset to return new videos from + Example Usage + ```py + for liked_video in api.user(username='public_likes'): + print(liked_video.id) + ``` """ ( region, diff --git a/TikTokApi/api/video.py b/TikTokApi/api/video.py index 26d8afdc..34a4e2cf 100644 --- a/TikTokApi/api/video.py +++ b/TikTokApi/api/video.py @@ -15,23 +15,27 @@ class Video: - """A TikTok Video class + """ + A TikTok Video class - Attributes - id: The TikTok video ID. - author: The author of the TikTok as a User object. - as_dict: The dictionary provided to create the class. + Example Usage + ```py + video = api.video(id='7041997751718137094') + ``` """ parent: ClassVar[TikTokApi] - # TODO: Use __getattribute__ - id: str + """TikTok's ID of the Video""" author: Optional[User] + """The User who created the Video""" sound: Optional[Sound] + """The Sound that is associated with the Video""" hashtags: Optional[list[Hashtag]] + """A List of Hashtags on the Video""" as_dict: dict + """The raw data associated with this Video.""" def __init__( self, @@ -39,6 +43,9 @@ def __init__( url: Optional[str] = None, data: Optional[dict] = None, ): + """ + You must provide the id or a valid url, else this will fail. + """ self.id = id if data is not None: self.as_dict = data @@ -50,10 +57,25 @@ def __init__( raise TypeError("You must provide id or url parameter.") def info(self, **kwargs) -> dict: + """ + Returns a dictionary of TikTok's Video object. + + Example Usage + ```py + video_data = api.video(id='7041997751718137094').info() + ``` + """ return self.info_full(**kwargs)["itemInfo"]["itemStruct"] def info_full(self, **kwargs) -> dict: - """Returns a dictionary of a specific TikTok.""" + """ + Returns a dictionary of all data associated with a TikTok Video. + + Example Usage + ```py + video_data = api.video(id='7041997751718137094').info_full() + ``` + """ ( region, language, @@ -74,6 +96,18 @@ def info_full(self, **kwargs) -> dict: return self.parent.get_data(path, **kwargs) def bytes(self, **kwargs) -> bytes: + """ + Returns the bytes of a TikTok Video. + + Example Usage + ```py + video_bytes = api.video(id='7041997751718137094').bytes() + + # Saving The Video + with open('saved_video.mp4', 'wb') as output: + output.write(video_bytes) + ``` + """ ( region, language, diff --git a/TikTokApi/browser_utilities/browser.py b/TikTokApi/browser_utilities/browser.py index 704d0362..0fbd44dc 100644 --- a/TikTokApi/browser_utilities/browser.py +++ b/TikTokApi/browser_utilities/browser.py @@ -10,16 +10,15 @@ from .browser_interface import BrowserInterface from urllib.parse import splitquery, parse_qs, parse_qsl - -# Import Detection From Stealth from ..utilities import LOGGER_NAME -from .get_acrawler import get_acrawler, get_tt_params_script +from .get_acrawler import _get_acrawler, _get_tt_params_script from playwright.sync_api import sync_playwright playwright = None logger = logging.getLogger(LOGGER_NAME) + def get_playwright(): global playwright if playwright is None: @@ -85,7 +84,7 @@ def __init__( logger.critical("Webkit launch failed", exc_info=True) raise e - context = self.create_context(set_useragent=True) + context = self._create_context(set_useragent=True) page = context.new_page() self.get_params(page) context.close() @@ -120,7 +119,7 @@ def get_params(self, page) -> None: self.width = page.evaluate("""() => { return screen.width; }""") self.height = page.evaluate("""() => { return screen.height; }""") - def create_context(self, set_useragent=False): + def _create_context(self, set_useragent=False): iphone = playwright.devices["iPhone 11 Pro"] iphone["viewport"] = { "width": random.randint(320, 1920), @@ -138,7 +137,7 @@ def create_context(self, set_useragent=False): return context - def base36encode(self, number, alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"): + def _base36encode(self, number, alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"): """Converts an integer to a base36 string.""" base36 = "" sign = "" @@ -159,7 +158,7 @@ def base36encode(self, number, alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"): def gen_verifyFp(self): chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[:] chars_len = len(chars) - scenario_title = self.base36encode(int(time.time() * 1000)) + scenario_title = self._base36encode(int(time.time() * 1000)) uuid = [0] * 36 uuid[8] = "_" uuid[13] = "_" @@ -180,7 +179,7 @@ def process(route): route.abort() tt_params = None - context = self.create_context() + context = self._create_context() page = context.new_page() if calc_tt_params: @@ -213,7 +212,7 @@ def process(route): url = "{}&verifyFp={}&device_id={}".format(url, verifyFp, device_id) - page.add_script_tag(content=get_acrawler()) + page.add_script_tag(content=_get_acrawler()) evaluatedPage = page.evaluate( '''() => { var url = "''' @@ -228,7 +227,7 @@ def process(route): url = "{}&_signature={}".format(url, evaluatedPage) if calc_tt_params: - page.add_script_tag(content=get_tt_params_script()) + page.add_script_tag(content=_get_tt_params_script()) tt_params = page.evaluate( """() => { @@ -242,7 +241,7 @@ def process(route): context.close() return (verifyFp, device_id, evaluatedPage, tt_params) - def clean_up(self): + def _clean_up(self): try: self.browser.close() except Exception: diff --git a/TikTokApi/browser_utilities/browser_interface.py b/TikTokApi/browser_utilities/browser_interface.py index a453c61a..fb4e9332 100644 --- a/TikTokApi/browser_utilities/browser_interface.py +++ b/TikTokApi/browser_utilities/browser_interface.py @@ -16,5 +16,5 @@ def sign_url(self, calc_tt_params=False, **kwargs): pass @abc.abstractmethod - def clean_up(self) -> None: + def _clean_up(self) -> None: pass diff --git a/TikTokApi/browser_utilities/get_acrawler.py b/TikTokApi/browser_utilities/get_acrawler.py index f1a51f16..7996dcfe 100644 --- a/TikTokApi/browser_utilities/get_acrawler.py +++ b/TikTokApi/browser_utilities/get_acrawler.py @@ -1,6 +1,6 @@ -def get_tt_params_script(): +def _get_tt_params_script(): return """var CryptoJS=CryptoJS||function(e,t){var r={},n=r.lib={},i=n.Base=function(){function e(){}return{extend:function(t){e.prototype=this;var r=new e;return t&&r.mixIn(t),r.hasOwnProperty("init")&&this.init!==r.init||(r.init=function(){r.$super.init.apply(this,arguments)}),r.init.prototype=r,r.$super=this,r},create:function(){var e=this.extend();return e.init.apply(e,arguments),e},init:function(){},mixIn:function(e){for(var t in e)e.hasOwnProperty(t)&&(this[t]=e[t]);e.hasOwnProperty("toString")&&(this.toString=e.toString)},clone:function(){return this.init.prototype.extend(this)}}}(),c=n.WordArray=i.extend({init:function(e,t){e=this.words=e||[],this.sigBytes=null!=t?t:4*e.length},toString:function(e){return(e||f).stringify(this)},concat:function(e){var t=this.words,r=e.words,n=this.sigBytes,i=e.sigBytes;if(this.clamp(),n%4)for(var c=0;c>>2]>>>24-c%4*8&255;t[n+c>>>2]|=o<<24-(n+c)%4*8}else if(r.length>65535)for(c=0;c>>2]=r[c>>>2];else t.push.apply(t,r);return this.sigBytes+=i,this},clamp:function(){var t=this.words,r=this.sigBytes;t[r>>>2]&=4294967295<<32-r%4*8,t.length=e.ceil(r/4)},clone:function(){var e=i.clone.call(this);return e.words=this.words.slice(0),e},random:function(t){for(var r,n=[],i=function(t){t=t;var r=987654321,n=4294967295;return function(){var i=((r=36969*(65535&r)+(r>>16)&n)<<16)+(t=18e3*(65535&t)+(t>>16)&n)&n;return i/=4294967296,(i+=.5)*(e.random()>.5?1:-1)}},o=0;o>>2]>>>24-i%4*8&255;n.push((c>>>4).toString(16)),n.push((15&c).toString(16))}return n.join("")},parse:function(e){for(var t=e.length,r=[],n=0;n>>3]|=parseInt(e.substr(n,2),16)<<24-n%8*4;return new c.init(r,t/2)}},a=o.Latin1={stringify:function(e){for(var t=e.words,r=e.sigBytes,n=[],i=0;i>>2]>>>24-i%4*8&255;n.push(String.fromCharCode(c))}return n.join("")},parse:function(e){for(var t=e.length,r=[],n=0;n>>2]|=(255&e.charCodeAt(n))<<24-n%4*8;return new c.init(r,t)}},s=o.Utf8={stringify:function(e){try{return decodeURIComponent(escape(a.stringify(e)))}catch(e){throw new Error("Malformed UTF-8 data")}},parse:function(e){return a.parse(unescape(encodeURIComponent(e)))}},u=n.BufferedBlockAlgorithm=i.extend({reset:function(){this._data=new c.init,this._nDataBytes=0},_append:function(e){"string"==typeof e&&(e=s.parse(e)),this._data.concat(e),this._nDataBytes+=e.sigBytes},_process:function(t){var r=this._data,n=r.words,i=r.sigBytes,o=this.blockSize,f=i/(4*o),a=(f=t?e.ceil(f):e.max((0|f)-this._minBufferSize,0))*o,s=e.min(4*a,i);if(a){for(var u=0;u>>2]>>>24-c%4*8&255)<<16|(t[c+1>>>2]>>>24-(c+1)%4*8&255)<<8|t[c+2>>>2]>>>24-(c+2)%4*8&255,f=0;f<4&&c+.75*f>>6*(3-f)&63));var a=n.charAt(64);if(a)for(;i.length%4;)i.push(a);return i.join("")},parse:function(e){var r=e.length,n=this._map,i=this._reverseMap;if(!i){i=this._reverseMap=[];for(var c=0;c>>6-o%4*2;i[c>>>2]|=(f|a)<<24-c%4*8,c++}return t.create(i,c)}(e,r,i)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}(),CryptoJS.lib.Cipher||function(e){var t=CryptoJS,r=t.lib,n=r.Base,i=r.WordArray,c=r.BufferedBlockAlgorithm,o=t.enc,f=(o.Utf8,o.Base64),a=t.algo.EvpKDF,s=r.Cipher=c.extend({cfg:n.extend(),createEncryptor:function(e,t){return this.create(this._ENC_XFORM_MODE,e,t)},createDecryptor:function(e,t){return this.create(this._DEC_XFORM_MODE,e,t)},init:function(e,t,r){this.cfg=this.cfg.extend(r),this._xformMode=e,this._key=t,this.reset()},reset:function(){c.reset.call(this),this._doReset()},process:function(e){return this._append(e),this._process()},finalize:function(e){return e&&this._append(e),this._doFinalize()},keySize:4,ivSize:4,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:function(){function e(e){return"string"==typeof e?_:v}return function(t){return{encrypt:function(r,n,i){return e(n).encrypt(t,r,n,i)},decrypt:function(r,n,i){return e(n).decrypt(t,r,n,i)}}}}()}),u=(r.StreamCipher=s.extend({_doFinalize:function(){return this._process(!0)},blockSize:1}),t.mode={}),d=r.BlockCipherMode=n.extend({createEncryptor:function(e,t){return this.Encryptor.create(e,t)},createDecryptor:function(e,t){return this.Decryptor.create(e,t)},init:function(e,t){this._cipher=e,this._iv=t}}),l=u.CBC=function(){var t=d.extend();function r(t,r,n){var i=this._iv;if(i){var c=i;this._iv=e}else c=this._prevBlock;for(var o=0;o>>2];e.sigBytes-=t}},h=(r.BlockCipher=s.extend({cfg:s.cfg.extend({mode:l,padding:p}),reset:function(){s.reset.call(this);var e=this.cfg,t=e.iv,r=e.mode;if(this._xformMode==this._ENC_XFORM_MODE)var n=r.createEncryptor;else{n=r.createDecryptor;this._minBufferSize=1}this._mode&&this._mode.__creator==n?this._mode.init(this,t&&t.words):(this._mode=n.call(r,this,t&&t.words),this._mode.__creator=n)},_doProcessBlock:function(e,t){this._mode.processBlock(e,t)},_doFinalize:function(){var e=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){e.pad(this._data,this.blockSize);var t=this._process(!0)}else{t=this._process(!0);e.unpad(t)}return t},blockSize:4}),r.CipherParams=n.extend({init:function(e){this.mixIn(e)},toString:function(e){return(e||this.formatter).stringify(this)}})),y=(t.format={}).OpenSSL={stringify:function(e){var t=e.ciphertext,r=e.salt;if(r)var n=i.create([1398893684,1701076831]).concat(r).concat(t);else n=t;return n.toString(f)},parse:function(e){var t=f.parse(e),r=t.words;if(1398893684==r[0]&&1701076831==r[1]){var n=i.create(r.slice(2,4));r.splice(0,4),t.sigBytes-=16}return h.create({ciphertext:t,salt:n})}},v=r.SerializableCipher=n.extend({cfg:n.extend({format:y}),encrypt:function(e,t,r,n){n=this.cfg.extend(n);var i=e.createEncryptor(r,n),c=i.finalize(t),o=i.cfg;return h.create({ciphertext:c,key:r,iv:o.iv,algorithm:e,mode:o.mode,padding:o.padding,blockSize:e.blockSize,formatter:n.format})},decrypt:function(e,t,r,n){return n=this.cfg.extend(n),t=this._parse(t,n.format),e.createDecryptor(r,n).finalize(t.ciphertext)},_parse:function(e,t){return"string"==typeof e?t.parse(e,this):e}}),b=(t.kdf={}).OpenSSL={execute:function(e,t,r,n){n||(n=i.random(8));var c=a.create({keySize:t+r}).compute(e,n),o=i.create(c.words.slice(t),4*r);return c.sigBytes=4*t,h.create({key:c,iv:o,salt:n})}},_=r.PasswordBasedCipher=v.extend({cfg:v.cfg.extend({kdf:b}),encrypt:function(e,t,r,n){var i=(n=this.cfg.extend(n)).kdf.execute(r,e.keySize,e.ivSize);n.iv=i.iv;var c=v.encrypt.call(this,e,t,i.key,n);return c.mixIn(i),c},decrypt:function(e,t,r,n){n=this.cfg.extend(n),t=this._parse(t,n.format);var i=n.kdf.execute(r,e.keySize,e.ivSize,t.salt);return n.iv=i.iv,v.decrypt.call(this,e,t,i.key,n)}})}(),CryptoJS.mode.ECB=function(){var e=CryptoJS.lib.BlockCipherMode.extend();return e.Encryptor=e.extend({processBlock:function(e,t){this._cipher.encryptBlock(e,t)}}),e.Decryptor=e.extend({processBlock:function(e,t){this._cipher.decryptBlock(e,t)}}),e}(),function(){var e=CryptoJS,t=e.lib.BlockCipher,r=e.algo,n=[],i=[],c=[],o=[],f=[],a=[],s=[],u=[],d=[],l=[];!function(){for(var e=[],t=0;t<256;t++)e[t]=t<128?t<<1:t<<1^283;var r=0,p=0;for(t=0;t<256;t++){var h=p^p<<1^p<<2^p<<3^p<<4;h=h>>>8^255&h^99,n[r]=h,i[h]=r;var y=e[r],v=e[y],b=e[v],_=257*e[h]^16843008*h;c[r]=_<<24|_>>>8,o[r]=_<<16|_>>>16,f[r]=_<<8|_>>>24,a[r]=_;_=16843009*b^65537*v^257*y^16843008*r;s[h]=_<<24|_>>>8,u[h]=_<<16|_>>>16,d[h]=_<<8|_>>>24,l[h]=_,r?(r=y^e[e[e[b^y]]],p^=e[e[p]]):r=p=1}}();var p=[0,1,2,4,8,16,32,64,128,27,54],h=r.AES=t.extend({_doReset:function(){if(!this._nRounds||this._keyPriorReset!==this._key){for(var e=this._keyPriorReset=this._key,t=e.words,r=e.sigBytes/4,i=4*((this._nRounds=r+6)+1),c=this._keySchedule=[],o=0;o6&&o%r==4&&(f=n[f>>>24]<<24|n[f>>>16&255]<<16|n[f>>>8&255]<<8|n[255&f]):(f=n[(f=f<<8|f>>>24)>>>24]<<24|n[f>>>16&255]<<16|n[f>>>8&255]<<8|n[255&f],f^=p[o/r|0]<<24),c[o]=c[o-r]^f}for(var a=this._invKeySchedule=[],h=0;h>>24]]^u[n[f>>>16&255]]^d[n[f>>>8&255]]^l[n[255&f]]}}},encryptBlock:function(e,t){this._doCryptBlock(e,t,this._keySchedule,c,o,f,a,n)},decryptBlock:function(e,t){var r=e[t+1];e[t+1]=e[t+3],e[t+3]=r,this._doCryptBlock(e,t,this._invKeySchedule,s,u,d,l,i);r=e[t+1];e[t+1]=e[t+3],e[t+3]=r},_doCryptBlock:function(e,t,r,n,i,c,o,f){for(var a=this._nRounds,s=e[t]^r[0],u=e[t+1]^r[1],d=e[t+2]^r[2],l=e[t+3]^r[3],p=4,h=1;h>>24]^i[u>>>16&255]^c[d>>>8&255]^o[255&l]^r[p++],v=n[u>>>24]^i[d>>>16&255]^c[l>>>8&255]^o[255&s]^r[p++],b=n[d>>>24]^i[l>>>16&255]^c[s>>>8&255]^o[255&u]^r[p++],_=n[l>>>24]^i[s>>>16&255]^c[u>>>8&255]^o[255&d]^r[p++];s=y,u=v,d=b,l=_}y=(f[s>>>24]<<24|f[u>>>16&255]<<16|f[d>>>8&255]<<8|f[255&l])^r[p++],v=(f[u>>>24]<<24|f[d>>>16&255]<<16|f[l>>>8&255]<<8|f[255&s])^r[p++],b=(f[d>>>24]<<24|f[l>>>16&255]<<16|f[s>>>8&255]<<8|f[255&u])^r[p++],_=(f[l>>>24]<<24|f[s>>>16&255]<<16|f[u>>>8&255]<<8|f[255&d])^r[p++];e[t]=y,e[t+1]=v,e[t+2]=b,e[t+3]=_},keySize:8});e.AES=t._createHelper(h)}();var a,i={};i.CryptoJS=CryptoJS,window._$jsvmprt=function(e,t,r){function n(e,t,r){return(n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(e){return!1}}()?Reflect.construct:function(e,t,r){var n=[null];n.push.apply(n,t);var i=new(Function.bind.apply(e,n));return r&&function(e,t){(Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}(i,r.prototype),i}).apply(null,arguments)}function i(e){return function(e){if(Array.isArray(e)){for(var t=0,r=new Array(e.length);t>7==0)return[1,i];if(i>>6==2){var c=parseInt(""+e[++t]+e[++t],16);return i&=63,[2,c=(i<<=8)+c]}if(i>>6==3){var o=parseInt(""+e[++t]+e[++t],16),f=parseInt(""+e[++t]+e[++t],16);return i&=63,[3,f=(i<<=16)+(o<<=8)+f]}},u=function(e,t){var r=parseInt(""+e[t]+e[t+1],16);return r>127?-256+r:r},d=function(e,t){var r=parseInt(""+e[t]+e[t+1]+e[t+2]+e[t+3],16);return r>32767?-65536+r:r},l=function(e,t){var r=parseInt(""+e[t]+e[t+1]+e[t+2]+e[t+3]+e[t+4]+e[t+5]+e[t+6]+e[t+7],16);return r>2147483647?0+r:r},p=function(e,t){return parseInt(""+e[t]+e[t+1],16)},h=function(e,t){return parseInt(""+e[t]+e[t+1]+e[t+2]+e[t+3],16)},y=y||this||window,v=(Object.keys,e.length,0),b="",_=v;_>=2,O>2)O=3&B,B>>=2,O<1?(O=B)<4?(g=k[C--],k[C]=k[C]-g):O<6?(g=k[C--],k[C]=k[C]===g):O<15&&(g=k[C],k[C]=k[C-1],k[C-1]=g):O<2?(O=B)<5&&(x=p(e,w),w+=2,g=l[x],k[++C]=g):O<3?(O=B)<6||(O<8?g=k[C--]:O<12&&(x=d(e,w),f[++a]=[[w+4,x-3],0,0],w+=2*x-2)):(O=B)<2?(g=k[C--],k[C]=k[C]1)if(O=3&B,B>>=2,O>2)(O=B)>5?(x=p(e,w),w+=2,k[++C]=l["$"+x]):O>3&&(x=d(e,w),f[a][0]&&!f[a][2]?f[a][1]=[w+4,x-3]:f[a++]=[0,[w+4,x-3],0],w+=2*x-2);else if(O>1){if((O=B)>2)if(k[C--])w+=4;else{if((x=d(e,w))<0){_=1,X(e,t,2*r),w+=2*x-2;break}w+=2*x-2}else if(O>0){for(x=h(e,w),g="",A=c.q[x][0];A0?(O=B)>1?(g=k[C--],k[C]=k[C]+g):O>-1&&(k[++C]=y):(O=B)>9?(x=p(e,w),w+=2,g=k[C--],l[x]=g):O>7?(x=h(e,w),w+=4,m=C+1,k[C-=x-1]=x?k.slice(C,m):[]):O>0&&(g=k[C--],k[C]=k[C]>g);else if(O>0){if(O=3&B,B>>=2,O<1){if((O=B)>9);else if(O>5)x=p(e,w),w+=2,k[C-=x]=0===x?new k[C]:n(k[C],i(k.slice(C+1,C+x+1)));else if(O>3){x=d(e,w);try{if(f[a][2]=1,1==(g=N(e,w+4,x-3,[],l,v,null,0))[0])return g}catch(b){if(f[a]&&f[a][1]&&1==(g=N(e,f[a][1][0],f[a][1][1],[],l,v,b,0))[0])return g}finally{if(f[a]&&f[a][0]&&1==(g=N(e,f[a][0][0],f[a][0][1],[],l,v,null,0))[0])return g;f[a]=0,a--}w+=2*x-2}}else if(O<2){if((O=B)>12)k[++C]=u(e,w),w+=2;else if(O>8){for(x=h(e,w),O="",A=c.q[x][0];A11?(g=k[C],k[++C]=g):O>0&&(k[++C]=g);else if((O=B)<1)k[C]=!k[C];else if(O<3){if((x=d(e,w))<0){_=1,X(e,t,2*r),w+=2*x-2;break}w+=2*x-2}}else if(O=3&B,B>>=2,O>2)(O=B)<1&&(k[++C]=null);else if(O>1){if((O=B)<9){for(g=k[C--],x=h(e,w),O="",A=c.q[x][0];A0)(O=B)<4?(m=k[C--],(O=k[C]).x===N?O.y>=1?k[C]=U(e,O.c,O.l,[m],O.z,S,null,1):(k[C]=U(e,O.c,O.l,[m],O.z,S,null,0),O.y++):k[C]=O(m)):O<6&&(k[C-=1]=k[C][k[C+1]]);else{if((O=B)<1)return[1,k[C--]];O<14?(m=k[C--],S=k[C--],(O=k[C--]).x===N?O.y>=1?k[++C]=U(e,O.c,O.l,m,O.z,S,null,1):(k[++C]=U(e,O.c,O.l,m,O.z,S,null,0),O.y++):k[++C]=O.apply(S,m)):O<16&&(x=d(e,w),(I=function t(){var r=arguments;return t.y>0||t.y++,U(e,t.c,t.l,r,t.z,this,null,0)}).c=w+4,I.l=x-2,I.x=N,I.y=0,I.z=l,k[C]=I,w+=2*x-2)}}if(_)for(;w>=2,O<1)if(O=3&B,B>>=2,O>2)(O=B)<1&&(k[++C]=null);else if(O>1){if((O=B)<9){for(g=k[C--],x=$[w],O="",A=c.q[x][0];A0)(O=B)<4?(m=k[C--],(O=k[C]).x===N?O.y>=1?k[C]=U(e,O.c,O.l,[m],O.z,S,null,1):(k[C]=U(e,O.c,O.l,[m],O.z,S,null,0),O.y++):k[C]=O(m)):O<6&&(k[C-=1]=k[C][k[C+1]]);else{var I;if((O=B)>14)x=$[w],(I=function t(){var r=arguments;return t.y>0||t.y++,U(e,t.c,t.l,r,t.z,this,null,0)}).c=w+4,I.l=x-2,I.x=N,I.y=0,I.z=l,k[C]=I,w+=2*x-2;else if(O>12)m=k[C--],S=k[C--],(O=k[C--]).x===N?O.y>=1?k[++C]=U(e,O.c,O.l,m,O.z,S,null,1):(k[++C]=U(e,O.c,O.l,m,O.z,S,null,0),O.y++):k[++C]=O.apply(S,m);else if(O>-1)return[1,k[C--]]}else if(O<2)if(O=3&B,B>>=2,O>2)(O=B)<1?k[C]=!k[C]:O<3&&(w+=2*(x=$[w])-2);else if(O>1)(O=B)<2?k[++C]=g:O<13&&(g=k[C],k[++C]=g);else if(O>0)if((O=B)<10){for(x=$[w],O="",A=c.q[x][0];A>=2,O<1)(O=B)>9?(x=$[w],w+=2,g=k[C--],l[x]=g):O>7?(x=$[w],w+=4,m=C+1,k[C-=x-1]=x?k.slice(C,m):[]):O>0&&(g=k[C--],k[C]=k[C]>g);else if(O<2)(O=B)>1?(g=k[C--],k[C]=k[C]+g):O>-1&&(k[++C]=y);else if(O<3)if((O=B)<2){for(x=$[w],g="",A=c.q[x][0];A5?(x=$[w],w+=2,k[++C]=l["$"+x]):O>3&&(x=$[w],f[a][0]&&!f[a][2]?f[a][1]=[w+4,x-3]:f[a++]=[0,[w+4,x-3],0],w+=2*x-2);else O=3&B,B>>=2,O<1?(O=B)<4?(g=k[C--],k[C]=k[C]-g):O<6?(g=k[C--],k[C]=k[C]===g):O<15&&(g=k[C],k[C]=k[C-1],k[C-1]=g):O<2?(O=B)<5&&(x=$[w],w+=2,g=l[x],k[++C]=g):O<3?(O=B)>10?(x=$[w],f[++a]=[[w+4,x-3],0,0],w+=2*x-2):O>6&&(g=k[C--]):(O=B)<2?(g=k[C--],k[C]=k[C]>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;switch(o){case 0:for(o=0,l=Math.pow(2,8),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;k=K(o)1:for(o=0,l=Math.pow(2,16),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;k=K(o)2:""}for(i[3]=k,m=k,b.push(k);;){if(z.index>S)"";for(o=0,l=Math.pow(2,j),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;switch(k=o){case 0:for(o=0,l=Math.pow(2,8),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;i[q]=K(o),k=q-1,J--1:for(o=0,l=Math.pow(2,16),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;i[q]=K(o),k=q-1,J--2:b.join("")}if(0==J&&(J=Math.pow(2,j),j),i[k])I=i[k]if(k!==q)null;I=m+m.charAt(0)}b.push(I),i[q]=m+I.charAt(0),m=I,0==--J&&(J=Math.pow(2,j),j)}}};y};""==typeof define&&define.amd?define({w}):"undefined"!=typeof module&&null!=module?module.exports=w:"undefined"!=typeof angular&&null!=angular&&angular.module("w",[]).factory("w",{w}),eval(w.x("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";'.replace(/[-]/g,function(m){return t[m.charCodeAt(0)&15]})}("var function ().length++return ));break;case ;else{".split("")))();""" diff --git a/TikTokApi/browser_utilities/stealth.py b/TikTokApi/browser_utilities/stealth.py deleted file mode 100644 index a9dc9e89..00000000 --- a/TikTokApi/browser_utilities/stealth.py +++ /dev/null @@ -1,502 +0,0 @@ -import re - - -def chrome_runtime(page) -> None: - page.evaluateOnNewDocument( - """ -() => { - window.chrome = { - runtime: {} - } -} -""" - ) - - -def console_debug(page) -> None: - page.evaluateOnNewDocument( - """ -() => { - window.console.debug = () => { - return null - } -} -""" - ) - - -def iframe_content_window(page) -> None: - page.evaluateOnNewDocument( - """ -() => { - try { - // Adds a contentWindow proxy to the provided iframe element - const addContentWindowProxy = iframe => { - const contentWindowProxy = { - get(target, key) { - // Now to the interesting part: - // We actually make this thing behave like a regular iframe window, - // by intercepting calls to e.g. `.self` and redirect it to the correct thing. :) - // That makes it possible for these assertions to be correct: - // iframe.contentWindow.self === window.top // must be false - if (key === 'self') { - return this - } - // iframe.contentWindow.frameElement === iframe // must be true - if (key === 'frameElement') { - return iframe - } - return Reflect.get(target, key) - } - } - if (!iframe.contentWindow) { - const proxy = new Proxy(window, contentWindowProxy) - Object.defineProperty(iframe, 'contentWindow', { - get() { - return proxy - }, - set(newValue) { - return newValue // contentWindow is immutable - }, - enumerable: true, - configurable: false - }) - } - } - // Handles iframe element creation, augments `srcdoc` property so we can intercept further - const handleIframeCreation = (target, thisArg, args) => { - const iframe = target.apply(thisArg, args) - // We need to keep the originals around - const _iframe = iframe - const _srcdoc = _iframe.srcdoc - // Add hook for the srcdoc property - // We need to be very surgical here to not break other iframes by accident - Object.defineProperty(iframe, 'srcdoc', { - configurable: true, // Important, so we can reset this later - get: function() { - return _iframe.srcdoc - }, - set: function(newValue) { - addContentWindowProxy(this) - // Reset property, the hook is only needed once - Object.defineProperty(iframe, 'srcdoc', { - configurable: false, - writable: false, - value: _srcdoc - }) - _iframe.srcdoc = newValue - } - }) - return iframe - } - // Adds a hook to intercept iframe creation events - const addIframeCreationSniffer = () => { - /* global document */ - const createElement = { - // Make toString() native - get(target, key) { - return Reflect.get(target, key) - }, - apply: function(target, thisArg, args) { - const isIframe = - args && args.length && `${args[0]}`.toLowerCase() === 'iframe' - if (!isIframe) { - // Everything as usual - return target.apply(thisArg, args) - } else { - return handleIframeCreation(target, thisArg, args) - } - } - } - // All this just due to iframes with srcdoc bug - document.createElement = new Proxy( - document.createElement, - createElement - ) - } - // Let's go - addIframeCreationSniffer() - } catch (err) { - // console.warn(err) - } -} -""" - ) - - -def media_codecs(page) -> None: - page.evaluateOnNewDocument( - """ - () => { - try { - /** - * Input might look funky, we need to normalize it so e.g. whitespace isn't an issue for our spoofing. - * - * @example - * video/webm; codecs="vp8, vorbis" - * video/mp4; codecs="avc1.42E01E" - * audio/x-m4a; - * audio/ogg; codecs="vorbis" - * @param {String} arg - */ - const parseInput = arg => { - const [mime, codecStr] = arg.trim().split(';') - let codecs = [] - if (codecStr && codecStr.includes('codecs="')) { - codecs = codecStr - .trim() - .replace(`codecs="`, '') - .replace(`"`, '') - .trim() - .split(',') - .filter(x => !!x) - .map(x => x.trim()) - } - return { mime, codecStr, codecs } - } - /* global HTMLMediaElement */ - const canPlayType = { - // Make toString() native - get(target, key) { - // Mitigate Chromium bug (#130) - if (typeof target[key] === 'function') { - return target[key].bind(target) - } - return Reflect.get(target, key) - }, - // Intercept certain requests - apply: function(target, ctx, args) { - if (!args || !args.length) { - return target.apply(ctx, args) - } - const { mime, codecs } = parseInput(args[0]) - // This specific mp4 codec is missing in Chromium - if (mime === 'video/mp4') { - if (codecs.includes('avc1.42E01E')) { - return 'probably' - } - } - // This mimetype is only supported if no codecs are specified - if (mime === 'audio/x-m4a' && !codecs.length) { - return 'maybe' - } - // This mimetype is only supported if no codecs are specified - if (mime === 'audio/aac' && !codecs.length) { - return 'probably' - } - // Everything else as usual - return target.apply(ctx, args) - } - } - HTMLMediaElement.prototype.canPlayType = new Proxy( - HTMLMediaElement.prototype.canPlayType, - canPlayType - ) - } catch (err) {} -} -""" - ) - - -def navigator_languages(page) -> None: - page.evaluateOnNewDocument( - """ -() => { - Object.defineProperty(navigator, 'languages', { - get: () => ['en-US', 'en'] - }) -} - """ - ) - - -def navigator_permissions(page) -> None: - page.evaluateOnNewDocument( - """ -() => { - const originalQuery = window.navigator.permissions.query - window.navigator.permissions.__proto__.query = parameters => - parameters.name === 'notifications' - ? Promise.resolve({ state: Notification.permission }) - : originalQuery(parameters) - const oldCall = Function.prototype.call - function call () { - return oldCall.apply(this, arguments) - } - Function.prototype.call = call - const nativeToStringFunctionString = Error.toString().replace( - /Error/g, - 'toString' - ) - const oldToString = Function.prototype.toString - function functionToString () { - if (this === window.navigator.permissions.query) { - return 'function query() { [native code] }' - } - if (this === functionToString) { - return nativeToStringFunctionString - } - return oldCall.call(oldToString, this) - } - Function.prototype.toString = functionToString -} - """ - ) - - -def navigator_plugins(page) -> None: - page.evaluateOnNewDocument( - """ -() => { - function mockPluginsAndMimeTypes() { - const makeFnsNative = (fns = []) => { - const oldCall = Function.prototype.call - function call() { - return oldCall.apply(this, arguments) - } - Function.prototype.call = call - const nativeToStringFunctionString = Error.toString().replace( - /Error/g, - 'toString' - ) - const oldToString = Function.prototype.toString - function functionToString() { - for (const fn of fns) { - if (this === fn.ref) { - return `function ${fn.name}() { [native code] }` - } - } - if (this === functionToString) { - return nativeToStringFunctionString - } - return oldCall.call(oldToString, this) - } - Function.prototype.toString = functionToString - } - const mockedFns = [] - const fakeData = { - mimeTypes: [ - { - type: 'application/pdf', - suffixes: 'pdf', - description: '', - __pluginName: 'Chrome PDF Viewer' - }, - { - type: 'application/x-google-chrome-pdf', - suffixes: 'pdf', - description: 'Portable Document Format', - __pluginName: 'Chrome PDF Plugin' - }, - { - type: 'application/x-nacl', - suffixes: '', - description: 'Native Client Executable', - enabledPlugin: Plugin, - __pluginName: 'Native Client' - }, - { - type: 'application/x-pnacl', - suffixes: '', - description: 'Portable Native Client Executable', - __pluginName: 'Native Client' - } - ], - plugins: [ - { - name: 'Chrome PDF Plugin', - filename: 'internal-pdf-viewer', - description: 'Portable Document Format' - }, - { - name: 'Chrome PDF Viewer', - filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', - description: '' - }, - { - name: 'Native Client', - filename: 'internal-nacl-plugin', - description: '' - } - ], - fns: { - namedItem: instanceName => { - const fn = function (name) { - if (!arguments.length) { - throw new TypeError( - `Failed to execute 'namedItem' on '${instanceName}': 1 argument required, but only 0 present.` - ) - } - return this[name] || null - } - mockedFns.push({ ref: fn, name: 'namedItem' }) - return fn - }, - item: instanceName => { - const fn = function (index) { - if (!arguments.length) { - throw new TypeError( - `Failed to execute 'namedItem' on '${instanceName}': 1 argument required, but only 0 present.` - ) - } - return this[index] || null - } - mockedFns.push({ ref: fn, name: 'item' }) - return fn - }, - refresh: instanceName => { - const fn = function () { - return undefined - } - mockedFns.push({ ref: fn, name: 'refresh' }) - return fn - } - } - } - const getSubset = (keys, obj) => - keys.reduce((a, c) => ({ ...a, [c]: obj[c] }), {}) - function generateMimeTypeArray() { - const arr = fakeData.mimeTypes - .map(obj => getSubset(['type', 'suffixes', 'description'], obj)) - .map(obj => Object.setPrototypeOf(obj, MimeType.prototype)) - arr.forEach(obj => { - arr[obj.type] = obj - }) - arr.namedItem = fakeData.fns.namedItem('MimeTypeArray') - arr.item = fakeData.fns.item('MimeTypeArray') - return Object.setPrototypeOf(arr, MimeTypeArray.prototype) - } - const mimeTypeArray = generateMimeTypeArray() - Object.defineProperty(navigator, 'mimeTypes', { - get: () => mimeTypeArray - }) - function generatePluginArray() { - const arr = fakeData.plugins - .map(obj => getSubset(['name', 'filename', 'description'], obj)) - .map(obj => { - const mimes = fakeData.mimeTypes.filter( - m => m.__pluginName === obj.name - ) - mimes.forEach((mime, index) => { - navigator.mimeTypes[mime.type].enabledPlugin = obj - obj[mime.type] = navigator.mimeTypes[mime.type] - obj[index] = navigator.mimeTypes[mime.type] - }) - obj.length = mimes.length - return obj - }) - .map(obj => { - obj.namedItem = fakeData.fns.namedItem('Plugin') - obj.item = fakeData.fns.item('Plugin') - return obj - }) - .map(obj => Object.setPrototypeOf(obj, Plugin.prototype)) - arr.forEach(obj => { - arr[obj.name] = obj - }) - arr.namedItem = fakeData.fns.namedItem('PluginArray') - arr.item = fakeData.fns.item('PluginArray') - arr.refresh = fakeData.fns.refresh('PluginArray') - return Object.setPrototypeOf(arr, PluginArray.prototype) - } - const pluginArray = generatePluginArray() - Object.defineProperty(navigator, 'plugins', { - get: () => pluginArray - }) - makeFnsNative(mockedFns) - } - try { - const isPluginArray = navigator.plugins instanceof PluginArray - const hasPlugins = isPluginArray && navigator.plugins.length > 0 - if (isPluginArray && hasPlugins) { - return - } - mockPluginsAndMimeTypes() - } catch (err) { } -} -""" - ) - - -def navigator_webdriver(page) -> None: - page.evaluateOnNewDocument( - """ -() => { - Object.defineProperty(window, 'navigator', { - value: new Proxy(navigator, { - has: (target, key) => (key === 'webdriver' ? false : key in target), - get: (target, key) => - key === 'webdriver' - ? undefined - : typeof target[key] === 'function' - ? target[key].bind(target) - : target[key] - }) - }) -} - """ - ) - - -def user_agent(page) -> None: - return - ua = page.browser.userAgent() - ua = ua.replace("HeadlessChrome", "Chrome") # hide headless nature - ua = re.sub( - r"\(([^)]+)\)", "(Windows NT 10.0; Win64; x64)", ua, 1 - ) # ensure windows - - page.setUserAgent(ua) - - -def webgl_vendor(page) -> None: - page.evaluateOnNewDocument( - """ -() => { - try { - const getParameter = WebGLRenderingContext.prototype.getParameter - WebGLRenderingContext.prototype.getParameter = function (parameter) { - if (parameter === 37445) { - return 'Intel Inc.' - } - if (parameter === 37446) { - return 'Intel Iris OpenGL Engine' - } - return getParameter.apply(this, [parameter]) - } - } catch (err) {} -} -""" - ) - - -def window_outerdimensions(page) -> None: - page.evaluateOnNewDocument( - """ -() => { - try { - if (window.outerWidth && window.outerHeight) { - return - } - const windowFrame = 85 - window.outerWidth = window.innerWidth - window.outerHeight = window.innerHeight + windowFrame - } catch (err) { } -} -""" - ) - - -def stealth(page) -> None: - # chrome_runtime(page) - console_debug(page) - iframe_content_window(page) - # navigator_languages(page) - navigator_permissions(page) - navigator_plugins(page) - navigator_webdriver(page) - # navigator_vendor(page) - user_agent(page) - webgl_vendor(page) - window_outerdimensions(page) - media_codecs(page) diff --git a/TikTokApi/tiktok.py b/TikTokApi/tiktok.py index 1ddfb1a3..38c95eb4 100644 --- a/TikTokApi/tiktok.py +++ b/TikTokApi/tiktok.py @@ -8,7 +8,13 @@ from urllib.parse import quote, urlencode import requests -from TikTokApi.api import sound, user, search, hashtag, video, trending +from .api.sound import Sound +from .api.user import User +from .api.search import Search +from .api.hashtag import Hashtag +from .api.video import Video +from .api.trending import Trending + from playwright.sync_api import sync_playwright from .exceptions import * @@ -25,6 +31,13 @@ class TikTokApi: _instance = None logger: ClassVar[logging.Logger] = logging.getLogger(LOGGER_NAME) + user = User + search = Search + sound = Sound + hashtag = Hashtag + video = Video + trending = Trending + @staticmethod def __new__(cls, logging_level=logging.WARNING, *args, **kwargs): """The TikTokApi class. Used to interact with TikTok. This is a singleton @@ -96,23 +109,12 @@ class to prevent issues from arising with playwright def _initialize(self, logging_level=logging.WARNING, **kwargs): # Add classes from the api folder - user.User.parent = self - self.user = user.User - - search.Search.parent = self - self.search = search.Search - - sound.Sound.parent = self - self.sound = sound.Sound - - hashtag.Hashtag.parent = self - self.hashtag = hashtag.Hashtag - - video.Video.parent = self - self.video = video.Video - - trending.Trending.parent = self - self.trending = trending.Trending + User.parent = self + Search.parent = self + Sound.parent = self + Hashtag.parent = self + Video.parent = self + Trending.parent = self self.logger.setLevel(level=logging_level) @@ -262,7 +264,7 @@ def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict: r = requests.get( url, headers=headers, - cookies=self.get_cookies(**kwargs), + cookies=self._get_cookies(**kwargs), proxies=self._format_proxy(proxy), **self.requests_extra_kwargs, ) @@ -276,7 +278,7 @@ def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict: self.logger.error( "Tiktok wants to display a captcha.\nResponse:\n%s\nCookies:\n%s", r.text, - self.get_cookies(**kwargs), + self._get_cookies(**kwargs), ) raise TikTokCaptchaError() @@ -350,14 +352,10 @@ def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict: self.logger.exception("Converting response to JSON failed") raise JSONDecodeFailure() from e - def clean_up(self): - """A basic cleanup method, called automatically from the code""" - self.__del__() - def __del__(self): """A basic cleanup method, called automatically from the code""" try: - self.browser.clean_up() + self.browser._clean_up() except Exception: pass try: @@ -405,7 +403,7 @@ def external_signer(self, url, custom_device_id=None, verifyFp=None): parsed_data["referrer"], ) - def get_cookies(self, **kwargs): + def _get_cookies(self, **kwargs): """Extracts cookies from the kwargs passed to the function for get_data""" device_id = kwargs.get( "custom_device_id", @@ -486,7 +484,7 @@ def get_bytes(self, **kwargs) -> bytes: "User-Agent": user_agent, }, proxies=self._format_proxy(proxy), - cookies=self.get_cookies(**kwargs), + cookies=self._get_cookies(**kwargs), ) return r.content diff --git a/TikTokApi/utilities.py b/TikTokApi/utilities.py index 61ee915b..4e3315df 100644 --- a/TikTokApi/utilities.py +++ b/TikTokApi/utilities.py @@ -3,6 +3,7 @@ LOGGER_NAME: str = "TikTokApi" + def update_messager(): if not check("TikTokApi"): # Outdated diff --git a/docs/TikTokApi.html b/docs/TikTokApi.html new file mode 100644 index 00000000..13a1e9fe --- /dev/null +++ b/docs/TikTokApi.html @@ -0,0 +1,420 @@ + + + + + + + TikTokApi API documentation + + + + + + + + +
+
+

+TikTokApi

+ +

Unofficial TikTok API in Python

+ +

This is an unofficial api wrapper for TikTok.com in python. With this api you are able to call most trending and fetch specific user information as well as much more.

+ +

DOI LinkedIn Sponsor Me GitHub release (latest by date) Build Status GitHub Downloads Support Server

+ +

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.

+ +

TikAPI | 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

+ + + +

Upgrading from V4 to V5

+ +

Documentation

+ +

You can find the full documentation TikTokApi.html">here, the TikTokApi Class is where you'll probably spend most of your time.

+ +

Getting Started

+ +

To get started using this api follow the instructions below.

+ +

How to Support The Project

+ +
    +
  • Star the repo 😎
  • +
  • Consider sponsoring me on GitHub
  • +
  • Send me an email or a LinkedIn message telling me what you're using the API for, I really like hearing what people are using it for.
  • +
  • Submit PRs for issues :)
  • +
+ +

Installing

+ +

If you run into an issue please check the closed issues on the github, although feel free to re-open a new issue if you find an issue that's been closed for a few months. The codebase can and does run into similar issues as it has before, because TikTok changes things up.

+ +
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 (TODO: new one for v5 coming soon) YouTube video just for that.

+ +

Docker Installation

+ +

Clone this repository onto a local machine (or just the Dockerfile since it installs TikTokApi from pip) then run the following commands.

+ +
docker pull mcr.microsoft.com/playwright:focal
+docker build . -t tiktokapi:latest
+docker run -v TikTokApi --rm tiktokapi:latest python3 your_script.py
+
+ +

Note this assumes your script is named your_script.py and lives in the root of this directory.

+ +

Common Issues

+ +

Please don't open an issue if you're experiencing one of these just comment if the provided solution do not work for you.

+ +
    +
  • Browser Has no Attribute - make sure you ran python3 -m playwright install, if your error persists try the playwright-python quickstart guide and diagnose issues from there.
  • +
+ +

Quick Start Guide

+ +

Here's a quick bit of code to get the most recent trending videos on TikTok. There's more examples in the examples directory.

+ +
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=zwLmLfVI-VQ for a visual
+# TODO: Update link
+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)
+
+ +

To run the example scripts from the repository root, make sure you use the -m option on python.

+ +
python -m examples.get_trending
+
+ +

You can access the dictionary type of an object using .as_dict. On a video this may look like +this, although TikTok changes their structure from time to time so it's worth investigating the structure of the dictionary when you use this package.

+ +

Upgrading from V4 to V5

+ +

All changes will be noted on #803 if you want more information.

+ +

Motivation

+ +

This package has been difficult to maintain due to it's structure, difficult to work with since the user of the package must write parsing methods to extract information from dictionaries, more memory intensive than it needs to be (although this can be further improved), and in general just difficult to work with for new users.

+ +

As a result, I've decided to at least attempt to remedy some of these issues, the biggest changes are that

+ +
    +
  1. The package has shifted to using classes for different TikTok objects resulting in an easier, higher-level programming experience.
  2. +
  3. All methods that used to return a list of objects have been switched to using generators, to hopefully decrease memory utilization for most users.
  4. +
+ +

Upgrading Examples

+ +

Accessing Dictionary on Objects (similar to V4)

+ +

TODO: Make video upgrading from V4-V5?

+ +

You'll probably need to use this beyond just for legacy support, since not all attributes are parsed out and attached +to the different objects.

+ +

You may want to use this as a workaround for legacy applications while you upgrade the rest of the app. I'd suggest that you do eventually upgrade to using the higher-level approach fully.

+ +
user = api.user(username='therock')
+user.as_dict # -> dict of the user_object
+for video in user.videos():
+    video.as_dict # -> dict of TikTok's video object as found when requesting the videos endpoint
+
+ +

Here's a few more examples that help illustrate the differences in the flow of the usage of the package with V5.

+ +
# V4
+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
+
+ +

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.

+ +
# V4
+trending_videos = api.by_trending()
+for video in trending_videos:
+    # The dictionary responses are also different depending on what endpoint you got them from
+    # So, it's usually more painful than this to deal with
+    trending_user = api.get_user(id=video['author']['id'], secUid=video['author']['secUid'])
+
+
+# V5
+# This is more complicated than above, but it illustrates the simplified approach
+for trending_video in api.trending.videos():
+    user_stats = trending_video.author.info_full['stats']
+    if user_stats['followerCount'] >= 10000:
+        # maybe save the user in a database
+
+
+ +
+ View Source +
"""
+.. include:: ../README.md
+"""
+__docformat__ = "restructuredtext"
+
+from TikTokApi.tiktok import TikTokApi
+
+ +
+ +
+
+ + \ No newline at end of file diff --git a/docs/TikTokApi/api.html b/docs/TikTokApi/api.html new file mode 100644 index 00000000..da167950 --- /dev/null +++ b/docs/TikTokApi/api.html @@ -0,0 +1,250 @@ + + + + + + + TikTokApi.api API documentation + + + + + + + + +
+
+

+TikTokApi.api

+ +

This module contains classes that all represent different types of data sent back by the TikTok servers.

+ +

The files within in module correspond to what type of object is described and all have different methods associated with them.

+
+ +
+ View Source +
"""
+This module contains classes that all represent different types of data sent back by the TikTok servers.
+
+The files within in module correspond to what type of object is described and all have different methods associated with them.
+"""
+
+ +
+ +
+
+ + \ No newline at end of file diff --git a/docs/TikTokApi/api/hashtag.html b/docs/TikTokApi/api/hashtag.html new file mode 100644 index 00000000..6ddef760 --- /dev/null +++ b/docs/TikTokApi/api/hashtag.html @@ -0,0 +1,838 @@ + + + + + + + TikTokApi.api.hashtag API documentation + + + + + + + + +
+
+

+TikTokApi.api.hashtag

+ + +
+ View Source +
from __future__ import annotations
+import logging
+
+from urllib.parse import urlencode
+from ..exceptions import *
+
+from typing import TYPE_CHECKING, ClassVar, Generator, Optional
+
+if TYPE_CHECKING:
+    from ..tiktok import TikTokApi
+    from .video import Video
+
+
+class Hashtag:
+    """
+    A TikTok Hashtag/Challenge.
+
+    Example Usage
+    ```py
+    hashtag = api.hashtag(name='funny')
+    ```
+    """
+
+    parent: ClassVar[TikTokApi]
+
+    id: str
+    """The ID of the hashtag"""
+    name: str
+    """The name of the hashtag (omiting the #)"""
+    as_dict: dict
+    """The raw data associated with this hashtag."""
+
+    def __init__(
+        self,
+        name: Optional[str] = None,
+        id: Optional[str] = None,
+        data: Optional[str] = None,
+    ):
+        """
+        You must provide the name or id of the hashtag.
+        """
+        self.name = name
+        self.id = id
+
+        if data is not None:
+            self.as_dict = data
+            self.__extract_from_data()
+
+    def info(self, **kwargs) -> dict:
+        """
+        Returns TikTok's dictionary representation of the hashtag object.
+        """
+        return self.info_full(**kwargs)["challengeInfo"]["challenge"]
+
+    def info_full(self, **kwargs) -> dict:
+        """
+        Returns all information sent by TikTok related to this hashtag.
+
+        Example Usage
+        ```py
+        hashtag_data = api.hashtag(name='funny').info_full()
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        if self.name is not None:
+            query = {"challengeName": self.name}
+        else:
+            query = {"challengeId": self.id}
+        path = "api/challenge/detail/?{}&{}".format(
+            self.parent._add_url_params(), urlencode(query)
+        )
+
+        data = self.parent.get_data(path, **kwargs)
+
+        if data["challengeInfo"].get("challenge") is None:
+            raise TikTokNotFoundError("Challenge {} does not exist".format(self.name))
+
+        return data
+
+    def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]:
+        """Returns a dictionary listing TikToks with a specific hashtag.
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+            - cursor (int): The unix epoch to get videos since. TODO: Check this is right
+
+        Example Usage
+        ```py
+        for video in api.hashtag(name='funny').videos():
+            # do something
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        if self.id is None:
+            self.id = self.info()["id"]
+
+        cursor = offset
+        page_size = 30
+
+        while cursor - offset < count:
+            query = {
+                "count": page_size,
+                "challengeID": self.id,
+                "cursor": cursor,
+            }
+            path = "api/challenge/item_list/?{}&{}".format(
+                self.parent._add_url_params(), urlencode(query)
+            )
+            res = self.parent.get_data(path, **kwargs)
+
+            for result in res.get("itemList", []):
+                yield self.parent.video(data=result)
+
+            if not res.get("hasMore", False):
+                self.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            cursor = int(res["cursor"])
+
+    def __extract_from_data(self):
+        data = self.as_dict
+        keys = data.keys()
+
+        if "title" in keys:
+            self.id = data["id"]
+            self.name = data["title"]
+
+        if None in (self.name, self.id):
+            Hashtag.parent.logger.error(
+                f"Failed to create Hashtag with data: {data}\nwhich has keys {data.keys()}"
+            )
+
+    def __repr__(self):
+        return self.__str__()
+
+    def __str__(self):
+        return f"TikTokApi.hashtag(id='{self.id}', name='{self.name}')"
+
+    def __getattr__(self, name):
+        if name in ["id", "name", "as_dict"]:
+            self.as_dict = self.info()
+            self.__extract_from_data()
+            return self.__getattribute__(name)
+
+        raise AttributeError(f"{name} doesn't exist on TikTokApi.api.Hashtag")
+
+ +
+ +
+
+
+ #   + + + class + Hashtag: +
+ +
+ View Source +
class Hashtag:
+    """
+    A TikTok Hashtag/Challenge.
+
+    Example Usage
+    ```py
+    hashtag = api.hashtag(name='funny')
+    ```
+    """
+
+    parent: ClassVar[TikTokApi]
+
+    id: str
+    """The ID of the hashtag"""
+    name: str
+    """The name of the hashtag (omiting the #)"""
+    as_dict: dict
+    """The raw data associated with this hashtag."""
+
+    def __init__(
+        self,
+        name: Optional[str] = None,
+        id: Optional[str] = None,
+        data: Optional[str] = None,
+    ):
+        """
+        You must provide the name or id of the hashtag.
+        """
+        self.name = name
+        self.id = id
+
+        if data is not None:
+            self.as_dict = data
+            self.__extract_from_data()
+
+    def info(self, **kwargs) -> dict:
+        """
+        Returns TikTok's dictionary representation of the hashtag object.
+        """
+        return self.info_full(**kwargs)["challengeInfo"]["challenge"]
+
+    def info_full(self, **kwargs) -> dict:
+        """
+        Returns all information sent by TikTok related to this hashtag.
+
+        Example Usage
+        ```py
+        hashtag_data = api.hashtag(name='funny').info_full()
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        if self.name is not None:
+            query = {"challengeName": self.name}
+        else:
+            query = {"challengeId": self.id}
+        path = "api/challenge/detail/?{}&{}".format(
+            self.parent._add_url_params(), urlencode(query)
+        )
+
+        data = self.parent.get_data(path, **kwargs)
+
+        if data["challengeInfo"].get("challenge") is None:
+            raise TikTokNotFoundError("Challenge {} does not exist".format(self.name))
+
+        return data
+
+    def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]:
+        """Returns a dictionary listing TikToks with a specific hashtag.
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+            - cursor (int): The unix epoch to get videos since. TODO: Check this is right
+
+        Example Usage
+        ```py
+        for video in api.hashtag(name='funny').videos():
+            # do something
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        if self.id is None:
+            self.id = self.info()["id"]
+
+        cursor = offset
+        page_size = 30
+
+        while cursor - offset < count:
+            query = {
+                "count": page_size,
+                "challengeID": self.id,
+                "cursor": cursor,
+            }
+            path = "api/challenge/item_list/?{}&{}".format(
+                self.parent._add_url_params(), urlencode(query)
+            )
+            res = self.parent.get_data(path, **kwargs)
+
+            for result in res.get("itemList", []):
+                yield self.parent.video(data=result)
+
+            if not res.get("hasMore", False):
+                self.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            cursor = int(res["cursor"])
+
+    def __extract_from_data(self):
+        data = self.as_dict
+        keys = data.keys()
+
+        if "title" in keys:
+            self.id = data["id"]
+            self.name = data["title"]
+
+        if None in (self.name, self.id):
+            Hashtag.parent.logger.error(
+                f"Failed to create Hashtag with data: {data}\nwhich has keys {data.keys()}"
+            )
+
+    def __repr__(self):
+        return self.__str__()
+
+    def __str__(self):
+        return f"TikTokApi.hashtag(id='{self.id}', name='{self.name}')"
+
+    def __getattr__(self, name):
+        if name in ["id", "name", "as_dict"]:
+            self.as_dict = self.info()
+            self.__extract_from_data()
+            return self.__getattribute__(name)
+
+        raise AttributeError(f"{name} doesn't exist on TikTokApi.api.Hashtag")
+
+ +
+ +

A TikTok Hashtag/Challenge.

+ +

Example Usage

+ +
hashtag = api.hashtag(name='funny')
+
+
+ + +
+
#   + + + Hashtag( + name: Optional[str] = None, + id: Optional[str] = None, + data: Optional[str] = None +) +
+ +
+ View Source +
    def __init__(
+        self,
+        name: Optional[str] = None,
+        id: Optional[str] = None,
+        data: Optional[str] = None,
+    ):
+        """
+        You must provide the name or id of the hashtag.
+        """
+        self.name = name
+        self.id = id
+
+        if data is not None:
+            self.as_dict = data
+            self.__extract_from_data()
+
+ +
+ +

You must provide the name or id of the hashtag.

+
+ + +
+
+
#   + + id: str +
+ +

The ID of the hashtag

+
+ + +
+
+
#   + + name: str +
+ +

The name of the hashtag (omiting the #)

+
+ + +
+
+
#   + + as_dict: dict +
+ +

The raw data associated with this hashtag.

+
+ + +
+
+
#   + + + def + info(self, **kwargs) -> dict: +
+ +
+ View Source +
    def info(self, **kwargs) -> dict:
+        """
+        Returns TikTok's dictionary representation of the hashtag object.
+        """
+        return self.info_full(**kwargs)["challengeInfo"]["challenge"]
+
+ +
+ +

Returns TikTok's dictionary representation of the hashtag object.

+
+ + +
+
+
#   + + + def + info_full(self, **kwargs) -> dict: +
+ +
+ View Source +
    def info_full(self, **kwargs) -> dict:
+        """
+        Returns all information sent by TikTok related to this hashtag.
+
+        Example Usage
+        ```py
+        hashtag_data = api.hashtag(name='funny').info_full()
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        if self.name is not None:
+            query = {"challengeName": self.name}
+        else:
+            query = {"challengeId": self.id}
+        path = "api/challenge/detail/?{}&{}".format(
+            self.parent._add_url_params(), urlencode(query)
+        )
+
+        data = self.parent.get_data(path, **kwargs)
+
+        if data["challengeInfo"].get("challenge") is None:
+            raise TikTokNotFoundError("Challenge {} does not exist".format(self.name))
+
+        return data
+
+ +
+ +

Returns all information sent by TikTok related to this hashtag.

+ +

Example Usage

+ +
hashtag_data = api.hashtag(name='funny').info_full()
+
+
+ + +
+
+
#   + + + def + videos( + self, + count=30, + offset=0, + **kwargs +) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]: +
+ +
+ View Source +
    def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]:
+        """Returns a dictionary listing TikToks with a specific hashtag.
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+            - cursor (int): The unix epoch to get videos since. TODO: Check this is right
+
+        Example Usage
+        ```py
+        for video in api.hashtag(name='funny').videos():
+            # do something
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        if self.id is None:
+            self.id = self.info()["id"]
+
+        cursor = offset
+        page_size = 30
+
+        while cursor - offset < count:
+            query = {
+                "count": page_size,
+                "challengeID": self.id,
+                "cursor": cursor,
+            }
+            path = "api/challenge/item_list/?{}&{}".format(
+                self.parent._add_url_params(), urlencode(query)
+            )
+            res = self.parent.get_data(path, **kwargs)
+
+            for result in res.get("itemList", []):
+                yield self.parent.video(data=result)
+
+            if not res.get("hasMore", False):
+                self.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            cursor = int(res["cursor"])
+
+ +
+ +

Returns a dictionary listing TikToks with a specific hashtag.

+ +
    +
  • Parameters: +
      +
    • count (int): The amount of videos you want returned.
    • +
    • cursor (int): The unix epoch to get videos since. TODO: Check this is right
    • +
  • +
+ +

Example Usage

+ +
for video in api.hashtag(name='funny').videos():
+    # do something
+
+
+ + +
+
+
+ + \ No newline at end of file diff --git a/docs/TikTokApi/api/search.html b/docs/TikTokApi/api/search.html new file mode 100644 index 00000000..fd42e69a --- /dev/null +++ b/docs/TikTokApi/api/search.html @@ -0,0 +1,1026 @@ + + + + + + + TikTokApi.api.search API documentation + + + + + + + + +
+
+

+TikTokApi.api.search

+ + +
+ View Source +
from __future__ import annotations
+
+from urllib.parse import urlencode
+
+from typing import TYPE_CHECKING, Generator
+
+from .user import User
+from .sound import Sound
+from .hashtag import Hashtag
+
+if TYPE_CHECKING:
+    from ..tiktok import TikTokApi
+
+import requests
+
+
+class Search:
+    """Contains static methods about searching."""
+
+    parent: TikTokApi
+
+    @staticmethod
+    def users(search_term, count=28, **kwargs) -> Generator[User, None, None]:
+        """
+        Searches for users.
+
+        - Parameters:
+            - search_term (str): The phrase you want to search for.
+            - count (int): The amount of videos you want returned.
+
+        Example Usage
+        ```py
+        for user in api.search.users('therock'):
+            # do something
+        ```
+        """
+        return Search.discover_type(search_term, prefix="user", count=count, **kwargs)
+
+    @staticmethod
+    def sounds(search_term, count=28, **kwargs) -> Generator[Sound, None, None]:
+        """
+        Searches for sounds.
+
+        - Parameters:
+            - search_term (str): The phrase you want to search for.
+            - count (int): The amount of videos you want returned.
+
+        Example Usage
+        ```py
+        for user in api.search.sounds('funny'):
+            # do something
+        ```
+        """
+        return Search.discover_type(search_term, prefix="music", count=count, **kwargs)
+
+    @staticmethod
+    def hashtags(search_term, count=28, **kwargs) -> Generator[Hashtag, None, None]:
+        """
+        Searches for hashtags/challenges.
+
+        - Parameters:
+            - search_term (str): The phrase you want to search for.
+            - count (int): The amount of videos you want returned.
+
+        Example Usage
+        ```py
+        for user in api.search.hashtags('funny'):
+            # do something
+        ```
+        """
+        return Search.discover_type(
+            search_term, prefix="challenge", count=count, **kwargs
+        )
+
+    @staticmethod
+    def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> list:
+        """
+        Searches for a specific type of object.
+        You should instead use the users/sounds/hashtags as they all use data
+        from this function.
+
+        - Parameters:
+            - search_term (str): The phrase you want to search for.
+            - prefix (str): either user|music|challenge
+            - count (int): The amount of videos you want returned.
+
+        Example Usage
+        ```py
+        for user in api.search.discover_type('therock', 'user'):
+            # do something
+        ```
+
+        """
+        # TODO: Investigate if this is actually working as expected. Doesn't seem to be
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = Search.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        cursor = offset
+        page_size = 28
+
+        while cursor - offset < count:
+            query = {
+                "discoverType": 0,
+                "needItemList": False,
+                "keyWord": search_term,
+                "offset": cursor,
+                "count": page_size,
+                "useRecommend": False,
+                "language": "en",
+            }
+            path = "api/discover/{}/?{}&{}".format(
+                prefix, Search.parent._add_url_params(), urlencode(query)
+            )
+            data = Search.parent.get_data(path, **kwargs)
+
+            for x in data.get("userInfoList", []):
+                yield User(data=x["user"])
+
+            for x in data.get("musicInfoList", []):
+                yield Sound(data=x["music"])
+
+            for x in data.get("challengeInfoList", []):
+                yield Hashtag(data=x["challenge"])
+
+            if int(data["offset"]) <= offset:
+                Search.parent.logger.info(
+                    "TikTok is not sending videos beyond this point."
+                )
+                return
+
+            offset = int(data["offset"])
+
+    @staticmethod
+    def users_alternate(search_term, count=28, offset=0, **kwargs) -> list:
+        """
+        Searches for users using an alternate endpoint than Search.users
+
+        - Parameters:
+            - search_term (str): The phrase you want to search for.
+            - count (int): The amount of videos you want returned.
+
+        Example Usage
+        ```py
+        for user in api.search.users_alternate('therock'):
+            # do something
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = Search.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        cursor = offset
+
+        spawn = requests.head(
+            "https://www.tiktok.com",
+            proxies=Search.parent._format_proxy(proxy),
+            **Search.parent.requests_extra_kwargs
+        )
+        ttwid = spawn.cookies["ttwid"]
+
+        # For some reason when <= it can be off by one.
+        while cursor - offset <= count:
+            query = {
+                "keyword": search_term,
+                "cursor": cursor,
+                "app_language": "en",
+            }
+            path = "api/search/{}/full/?{}&{}".format(
+                "user", Search.parent._add_url_params(), urlencode(query)
+            )
+
+            data = Search.parent.get_data(
+                path, use_desktop_base_url=True, ttwid=ttwid, **kwargs
+            )
+
+            # When I move to 3.10+ support make this a match switch.
+            for result in data.get("user_list", []):
+                yield User(data=result)
+
+            if data.get("has_more", 0) == 0:
+                Search.parent.logger.info(
+                    "TikTok is not sending videos beyond this point."
+                )
+                return
+
+            cursor = int(data.get("cursor"))
+
+ +
+ +
+ +
+ + \ No newline at end of file diff --git a/docs/TikTokApi/api/sound.html b/docs/TikTokApi/api/sound.html new file mode 100644 index 00000000..23545be7 --- /dev/null +++ b/docs/TikTokApi/api/sound.html @@ -0,0 +1,926 @@ + + + + + + + TikTokApi.api.sound API documentation + + + + + + + + +
+
+

+TikTokApi.api.sound

+ + +
+ View Source +
from __future__ import annotations
+from os import path
+
+import requests
+import json
+
+from urllib.parse import quote, urlencode
+
+from ..helpers import extract_tag_contents
+from ..exceptions import *
+
+from typing import TYPE_CHECKING, ClassVar, Generator, Optional
+
+if TYPE_CHECKING:
+    from ..tiktok import TikTokApi
+    from .user import User
+    from .video import Video
+
+
+class Sound:
+    """
+    A TikTok Sound/Music/Song.
+
+    Example Usage
+    ```py
+    song = api.song(id='7016547803243022337')
+    ```
+    """
+
+    parent: ClassVar[TikTokApi]
+
+    id: str
+    """TikTok's ID for the sound"""
+    title: Optional[str]
+    """The title of the song."""
+    author: Optional[User]
+    """The author of the song (if it exists)"""
+
+    def __init__(self, id: Optional[str] = None, data: Optional[str] = None):
+        """
+        You must provide the id of the sound or it will not work.
+        """
+        if data is not None:
+            self.as_dict = data
+            self.__extract_from_data()
+        elif id is None:
+            raise TypeError("You must provide id parameter.")
+        else:
+            self.id = id
+
+    def info(self, use_html=False, **kwargs) -> dict:
+        """
+        Returns a dictionary of TikTok's Sound/Music object.
+
+        - Parameters:
+            - use_html (bool): If you want to perform an HTML request or not.
+                Defaults to False to use an API call, which shouldn't get detected
+                as often as an HTML request.
+
+
+        Example Usage
+        ```py
+        sound_data = api.sound(id='7016547803243022337').info()
+        ```
+        """
+        if use_html:
+            return self.info_full(**kwargs)["musicInfo"]
+
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        path = "node/share/music/-{}?{}".format(self.id, self.parent._add_url_params())
+        res = self.parent.get_data(path, **kwargs)
+
+        if res.get("statusCode", 200) == 10203:
+            raise TikTokNotFoundError()
+
+        return res["musicInfo"]["music"]
+
+    def info_full(self, **kwargs) -> dict:
+        """
+        Returns all the data associated with a TikTok Sound.
+
+        This makes an API request, there is no HTML request option, as such
+        with Sound.info()
+
+        Example Usage
+        ```py
+        sound_data = api.sound(id='7016547803243022337').info_full()
+        ```
+        """
+        r = requests.get(
+            "https://www.tiktok.com/music/-{}".format(self.id),
+            headers={
+                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
+                "Accept-Encoding": "gzip, deflate",
+                "Connection": "keep-alive",
+                "User-Agent": self.parent.user_agent,
+            },
+            proxies=self.parent._format_proxy(kwargs.get("proxy", None)),
+            cookies=self.parent._get_cookies(**kwargs),
+            **self.parent.requests_extra_kwargs,
+        )
+
+        data = extract_tag_contents(r.text)
+        return json.loads(data)["props"]["pageProps"]["musicInfo"]
+
+    def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]:
+        """
+        Returns Video objects of videos created with this sound.
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+            - cursor (int): The unix epoch to get videos since. TODO: Check this is right
+
+        Example Usage
+        ```py
+        for video in api.sound(id='7016547803243022337').videos():
+            # do something
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        cursor = offset
+        page_size = 30
+
+        while cursor - offset < count:
+            query = {
+                "secUid": "",
+                "musicID": self.id,
+                "cursor": cursor,
+                "shareUid": "",
+                "count": page_size,
+            }
+            path = "api/music/item_list/?{}&{}".format(
+                self.parent._add_url_params(), urlencode(query)
+            )
+
+            res = self.parent.get_data(path, send_tt_params=True, **kwargs)
+
+            for result in res.get("itemList", []):
+                yield self.parent.video(data=result)
+
+            if not res.get("hasMore", False):
+                self.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            cursor = int(res["cursor"])
+
+    def __extract_from_data(self):
+        data = self.as_dict
+        keys = data.keys()
+
+        if "authorName" in keys:
+            self.id = data["id"]
+            self.title = data["title"]
+
+            if data.get("authorName") is not None:
+                self.author = self.parent.user(username=data["authorName"])
+
+        if self.id is None:
+            Sound.parent.logger.error(
+                f"Failed to create Sound with data: {data}\nwhich has keys {data.keys()}"
+            )
+
+    def __repr__(self):
+        return self.__str__()
+
+    def __str__(self):
+        return f"TikTokApi.sound(id='{self.id}')"
+
+    def __getattr__(self, name):
+        if name in ["title", "author", "as_dict"]:
+            self.as_dict = self.info()
+            self.__extract_from_data()
+            return self.__getattribute__(name)
+
+        raise AttributeError(f"{name} doesn't exist on TikTokApi.api.Sound")
+
+ +
+ +
+
+
+ #   + + + class + Sound: +
+ +
+ View Source +
class Sound:
+    """
+    A TikTok Sound/Music/Song.
+
+    Example Usage
+    ```py
+    song = api.song(id='7016547803243022337')
+    ```
+    """
+
+    parent: ClassVar[TikTokApi]
+
+    id: str
+    """TikTok's ID for the sound"""
+    title: Optional[str]
+    """The title of the song."""
+    author: Optional[User]
+    """The author of the song (if it exists)"""
+
+    def __init__(self, id: Optional[str] = None, data: Optional[str] = None):
+        """
+        You must provide the id of the sound or it will not work.
+        """
+        if data is not None:
+            self.as_dict = data
+            self.__extract_from_data()
+        elif id is None:
+            raise TypeError("You must provide id parameter.")
+        else:
+            self.id = id
+
+    def info(self, use_html=False, **kwargs) -> dict:
+        """
+        Returns a dictionary of TikTok's Sound/Music object.
+
+        - Parameters:
+            - use_html (bool): If you want to perform an HTML request or not.
+                Defaults to False to use an API call, which shouldn't get detected
+                as often as an HTML request.
+
+
+        Example Usage
+        ```py
+        sound_data = api.sound(id='7016547803243022337').info()
+        ```
+        """
+        if use_html:
+            return self.info_full(**kwargs)["musicInfo"]
+
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        path = "node/share/music/-{}?{}".format(self.id, self.parent._add_url_params())
+        res = self.parent.get_data(path, **kwargs)
+
+        if res.get("statusCode", 200) == 10203:
+            raise TikTokNotFoundError()
+
+        return res["musicInfo"]["music"]
+
+    def info_full(self, **kwargs) -> dict:
+        """
+        Returns all the data associated with a TikTok Sound.
+
+        This makes an API request, there is no HTML request option, as such
+        with Sound.info()
+
+        Example Usage
+        ```py
+        sound_data = api.sound(id='7016547803243022337').info_full()
+        ```
+        """
+        r = requests.get(
+            "https://www.tiktok.com/music/-{}".format(self.id),
+            headers={
+                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
+                "Accept-Encoding": "gzip, deflate",
+                "Connection": "keep-alive",
+                "User-Agent": self.parent.user_agent,
+            },
+            proxies=self.parent._format_proxy(kwargs.get("proxy", None)),
+            cookies=self.parent._get_cookies(**kwargs),
+            **self.parent.requests_extra_kwargs,
+        )
+
+        data = extract_tag_contents(r.text)
+        return json.loads(data)["props"]["pageProps"]["musicInfo"]
+
+    def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]:
+        """
+        Returns Video objects of videos created with this sound.
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+            - cursor (int): The unix epoch to get videos since. TODO: Check this is right
+
+        Example Usage
+        ```py
+        for video in api.sound(id='7016547803243022337').videos():
+            # do something
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        cursor = offset
+        page_size = 30
+
+        while cursor - offset < count:
+            query = {
+                "secUid": "",
+                "musicID": self.id,
+                "cursor": cursor,
+                "shareUid": "",
+                "count": page_size,
+            }
+            path = "api/music/item_list/?{}&{}".format(
+                self.parent._add_url_params(), urlencode(query)
+            )
+
+            res = self.parent.get_data(path, send_tt_params=True, **kwargs)
+
+            for result in res.get("itemList", []):
+                yield self.parent.video(data=result)
+
+            if not res.get("hasMore", False):
+                self.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            cursor = int(res["cursor"])
+
+    def __extract_from_data(self):
+        data = self.as_dict
+        keys = data.keys()
+
+        if "authorName" in keys:
+            self.id = data["id"]
+            self.title = data["title"]
+
+            if data.get("authorName") is not None:
+                self.author = self.parent.user(username=data["authorName"])
+
+        if self.id is None:
+            Sound.parent.logger.error(
+                f"Failed to create Sound with data: {data}\nwhich has keys {data.keys()}"
+            )
+
+    def __repr__(self):
+        return self.__str__()
+
+    def __str__(self):
+        return f"TikTokApi.sound(id='{self.id}')"
+
+    def __getattr__(self, name):
+        if name in ["title", "author", "as_dict"]:
+            self.as_dict = self.info()
+            self.__extract_from_data()
+            return self.__getattribute__(name)
+
+        raise AttributeError(f"{name} doesn't exist on TikTokApi.api.Sound")
+
+ +
+ +

A TikTok Sound/Music/Song.

+ +

Example Usage

+ +
song = api.song(id='7016547803243022337')
+
+
+ + +
+
#   + + + Sound(id: Optional[str] = None, data: Optional[str] = None) +
+ +
+ View Source +
    def __init__(self, id: Optional[str] = None, data: Optional[str] = None):
+        """
+        You must provide the id of the sound or it will not work.
+        """
+        if data is not None:
+            self.as_dict = data
+            self.__extract_from_data()
+        elif id is None:
+            raise TypeError("You must provide id parameter.")
+        else:
+            self.id = id
+
+ +
+ +

You must provide the id of the sound or it will not work.

+
+ + +
+
+
#   + + id: str +
+ +

TikTok's ID for the sound

+
+ + +
+
+
#   + + title: Optional[str] +
+ +

The title of the song.

+
+ + +
+
+
#   + + author: Optional[TikTokApi.api.user.User] +
+ +

The author of the song (if it exists)

+
+ + +
+
+
#   + + + def + info(self, use_html=False, **kwargs) -> dict: +
+ +
+ View Source +
    def info(self, use_html=False, **kwargs) -> dict:
+        """
+        Returns a dictionary of TikTok's Sound/Music object.
+
+        - Parameters:
+            - use_html (bool): If you want to perform an HTML request or not.
+                Defaults to False to use an API call, which shouldn't get detected
+                as often as an HTML request.
+
+
+        Example Usage
+        ```py
+        sound_data = api.sound(id='7016547803243022337').info()
+        ```
+        """
+        if use_html:
+            return self.info_full(**kwargs)["musicInfo"]
+
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        path = "node/share/music/-{}?{}".format(self.id, self.parent._add_url_params())
+        res = self.parent.get_data(path, **kwargs)
+
+        if res.get("statusCode", 200) == 10203:
+            raise TikTokNotFoundError()
+
+        return res["musicInfo"]["music"]
+
+ +
+ +

Returns a dictionary of TikTok's Sound/Music object.

+ +
    +
  • Parameters: +
      +
    • use_html (bool): If you want to perform an HTML request or not. +Defaults to False to use an API call, which shouldn't get detected +as often as an HTML request.
    • +
  • +
+ +

Example Usage

+ +
sound_data = api.sound(id='7016547803243022337').info()
+
+
+ + +
+
+
#   + + + def + info_full(self, **kwargs) -> dict: +
+ +
+ View Source +
    def info_full(self, **kwargs) -> dict:
+        """
+        Returns all the data associated with a TikTok Sound.
+
+        This makes an API request, there is no HTML request option, as such
+        with Sound.info()
+
+        Example Usage
+        ```py
+        sound_data = api.sound(id='7016547803243022337').info_full()
+        ```
+        """
+        r = requests.get(
+            "https://www.tiktok.com/music/-{}".format(self.id),
+            headers={
+                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
+                "Accept-Encoding": "gzip, deflate",
+                "Connection": "keep-alive",
+                "User-Agent": self.parent.user_agent,
+            },
+            proxies=self.parent._format_proxy(kwargs.get("proxy", None)),
+            cookies=self.parent._get_cookies(**kwargs),
+            **self.parent.requests_extra_kwargs,
+        )
+
+        data = extract_tag_contents(r.text)
+        return json.loads(data)["props"]["pageProps"]["musicInfo"]
+
+ +
+ +

Returns all the data associated with a TikTok Sound.

+ +

This makes an API request, there is no HTML request option, as such +with Sound.info()

+ +

Example Usage

+ +
sound_data = api.sound(id='7016547803243022337').info_full()
+
+
+ + +
+
+
#   + + + def + videos( + self, + count=30, + offset=0, + **kwargs +) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]: +
+ +
+ View Source +
    def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]:
+        """
+        Returns Video objects of videos created with this sound.
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+            - cursor (int): The unix epoch to get videos since. TODO: Check this is right
+
+        Example Usage
+        ```py
+        for video in api.sound(id='7016547803243022337').videos():
+            # do something
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        cursor = offset
+        page_size = 30
+
+        while cursor - offset < count:
+            query = {
+                "secUid": "",
+                "musicID": self.id,
+                "cursor": cursor,
+                "shareUid": "",
+                "count": page_size,
+            }
+            path = "api/music/item_list/?{}&{}".format(
+                self.parent._add_url_params(), urlencode(query)
+            )
+
+            res = self.parent.get_data(path, send_tt_params=True, **kwargs)
+
+            for result in res.get("itemList", []):
+                yield self.parent.video(data=result)
+
+            if not res.get("hasMore", False):
+                self.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            cursor = int(res["cursor"])
+
+ +
+ +

Returns Video objects of videos created with this sound.

+ +
    +
  • Parameters: +
      +
    • count (int): The amount of videos you want returned.
    • +
    • cursor (int): The unix epoch to get videos since. TODO: Check this is right
    • +
  • +
+ +

Example Usage

+ +
for video in api.sound(id='7016547803243022337').videos():
+    # do something
+
+
+ + +
+
+
+ + \ No newline at end of file diff --git a/docs/TikTokApi/api/trending.html b/docs/TikTokApi/api/trending.html new file mode 100644 index 00000000..69d34c7d --- /dev/null +++ b/docs/TikTokApi/api/trending.html @@ -0,0 +1,495 @@ + + + + + + + TikTokApi.api.trending API documentation + + + + + + + + +
+
+

+TikTokApi.api.trending

+ + +
+ View Source +
from __future__ import annotations
+
+import logging
+import requests
+from urllib.parse import urlencode
+
+from .video import Video
+
+from typing import TYPE_CHECKING, Generator
+
+if TYPE_CHECKING:
+    from ..tiktok import TikTokApi
+
+
+class Trending:
+    """Contains static methods related to trending."""
+
+    parent: TikTokApi
+
+    @staticmethod
+    def videos(count=30, **kwargs) -> Generator[Video, None, None]:
+        """
+        Returns Videos that are trending on TikTok.
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+        """
+
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = Trending.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        spawn = requests.head(
+            "https://www.tiktok.com",
+            proxies=Trending.parent._format_proxy(proxy),
+            **Trending.parent.requests_extra_kwargs,
+        )
+        ttwid = spawn.cookies["ttwid"]
+
+        first = True
+        amount_yielded = 0
+
+        while amount_yielded < count:
+            query = {
+                "count": 30,
+                "id": 1,
+                "sourceType": 12,
+                "itemID": 1,
+                "insertedItemID": "",
+                "region": region,
+                "priority_region": region,
+                "language": language,
+            }
+            path = "api/recommend/item_list/?{}&{}".format(
+                Trending.parent._add_url_params(), urlencode(query)
+            )
+            res = Trending.parent.get_data(path, ttwid=ttwid, **kwargs)
+            for result in res.get("itemList", []):
+                yield Video(data=result)
+            amount_yielded += len(res.get("itemList", []))
+
+            if not res.get("hasMore", False) and not first:
+                Trending.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            first = False
+
+ +
+ +
+ +
+ + \ No newline at end of file diff --git a/docs/TikTokApi/api/user.html b/docs/TikTokApi/api/user.html new file mode 100644 index 00000000..839a4f69 --- /dev/null +++ b/docs/TikTokApi/api/user.html @@ -0,0 +1,1276 @@ + + + + + + + TikTokApi.api.user API documentation + + + + + + + + +
+
+

+TikTokApi.api.user

+ + +
+ View Source +
from __future__ import annotations
+
+import json
+import requests
+
+from urllib.parse import quote, urlencode
+
+from ..exceptions import *
+from ..helpers import extract_tag_contents
+
+from typing import TYPE_CHECKING, ClassVar, Generator, Optional
+
+if TYPE_CHECKING:
+    from ..tiktok import TikTokApi
+    from .video import Video
+
+
+class User:
+    """
+    A TikTok User.
+
+    Example Usage
+    ```py
+    user = api.user(username='therock')
+    # or
+    user_id = '5831967'
+    sec_uid = 'MS4wLjABAAAA-VASjiXTh7wDDyXvjk10VFhMWUAoxr8bgfO1kAL1-9s'
+    user = api.user(user_id=user_id, sec_uid=sec_uid)
+    ```
+
+    """
+
+    parent: ClassVar[TikTokApi]
+
+    user_id: str
+    """The user ID of the user."""
+    sec_uid: str
+    """The sec UID of the user."""
+    username: str
+    """The username of the user."""
+    as_dict: dict
+    """The raw data associated with this user."""
+
+    def __init__(
+        self,
+        username: Optional[str] = None,
+        user_id: Optional[str] = None,
+        sec_uid: Optional[str] = None,
+        data: Optional[str] = None,
+    ):
+        """
+        You must provide the username or (user_id and sec_uid) otherwise this
+        will not function correctly.
+        """
+        self.__update_id_sec_uid_username(user_id, sec_uid, username)
+        if data is not None:
+            self.as_dict = data
+            self.__extract_from_data()
+
+    def info(self, **kwargs):
+        """
+        Returns a dictionary of TikTok's User object
+
+        Example Usage
+        ```py
+        user_data = api.user(username='therock').info()
+        ```
+        """
+        return self.info_full(**kwargs)["user"]
+
+    def info_full(self, **kwargs) -> dict:
+        """
+        Returns a dictionary of information associated with this User.
+        Includes statistics about this user.
+
+        Example Usage
+        ```py
+        user_data = api.user(username='therock').info_full()
+        ```
+        """
+
+        # TODO: Find the one using only user_id & sec_uid
+        if not self.username:
+            raise TypeError(
+                "You must provide the username when creating this class to use this method."
+            )
+
+        quoted_username = quote(self.username)
+        r = requests.get(
+            "https://tiktok.com/@{}?lang=en".format(quoted_username),
+            headers={
+                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
+                "path": "/@{}".format(quoted_username),
+                "Accept-Encoding": "gzip, deflate",
+                "Connection": "keep-alive",
+                "User-Agent": self.parent.user_agent,
+            },
+            proxies=User.parent._format_proxy(kwargs.get("proxy", None)),
+            cookies=User.parent._get_cookies(**kwargs),
+            **User.parent.requests_extra_kwargs,
+        )
+
+        data = extract_tag_contents(r.text)
+        user = json.loads(data)
+
+        user_props = user["props"]["pageProps"]
+        if user_props["statusCode"] == 404:
+            raise TikTokNotFoundError(
+                "TikTok user with username {} does not exist".format(self.username)
+            )
+
+        return user_props["userInfo"]
+
+    def videos(self, count=30, cursor=0, **kwargs) -> Generator[Video, None, None]:
+        """
+        Returns a Generator yielding Video objects.
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+            - cursor (int): The unix epoch to get videos since. TODO: Check this is right
+
+        Example Usage
+        ```py
+        user = api.user(username='therock')
+        for video in user.videos(count=100):
+            print(video.id)
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = User.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        if not self.user_id and not self.sec_uid:
+            self.__find_attributes()
+
+        first = True
+        amount_yielded = 0
+
+        while amount_yielded < count:
+            query = {
+                "count": 30,
+                "id": self.user_id,
+                "cursor": cursor,
+                "type": 1,
+                "secUid": self.sec_uid,
+                "sourceType": 8,
+                "appId": 1233,
+                "region": region,
+                "priority_region": region,
+                "language": language,
+            }
+            path = "api/post/item_list/?{}&{}".format(
+                User.parent._add_url_params(), urlencode(query)
+            )
+
+            res = User.parent.get_data(path, send_tt_params=True, **kwargs)
+
+            videos = res.get("itemList", [])
+            amount_yielded += len(videos)
+            for video in videos:
+                yield self.parent.video(data=video)
+
+            if not res.get("hasMore", False) and not first:
+                User.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            cursor = res["cursor"]
+            first = False
+
+    def liked(
+        self, count: int = 30, cursor: int = 0, **kwargs
+    ) -> Generator[Video, None, None]:
+        """
+        Returns a dictionary listing TikToks that a given a user has liked.
+
+        **Note**: The user's likes must be **public** (which is not the default option)
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+            - cursor (int): The unix epoch to get videos since. TODO: Check this is right
+
+        Example Usage
+        ```py
+        for liked_video in api.user(username='public_likes'):
+            print(liked_video.id)
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        amount_yielded = 0
+        first = True
+
+        if self.user_id is None and self.sec_uid is None:
+            self.__find_attributes()
+
+        while amount_yielded < count:
+            query = {
+                "count": 30,
+                "id": self.user_id,
+                "type": 2,
+                "secUid": self.sec_uid,
+                "cursor": cursor,
+                "sourceType": 9,
+                "appId": 1233,
+                "region": region,
+                "priority_region": region,
+                "language": language,
+            }
+            path = "api/favorite/item_list/?{}&{}".format(
+                User.parent._add_url_params(), urlencode(query)
+            )
+
+            res = self.parent.get_data(path, **kwargs)
+
+            if "itemList" not in res.keys():
+                User.parent.logger.error("User's likes are most likely private")
+                return
+
+            videos = res.get("itemList", [])
+            amount_yielded += len(videos)
+            for video in videos:
+                amount_yielded += 1
+                yield self.parent.video(data=video)
+
+            if not res.get("hasMore", False) and not first:
+                User.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            cursor = res["cursor"]
+            first = False
+
+    def __extract_from_data(self):
+        data = self.as_dict
+        keys = data.keys()
+
+        if "user_info" in keys:
+            self.__update_id_sec_uid_username(
+                data["user_info"]["uid"],
+                data["user_info"]["sec_uid"],
+                data["user_info"]["unique_id"],
+            )
+        elif "uniqueId" in keys:
+            self.__update_id_sec_uid_username(
+                data["id"], data["secUid"], data["uniqueId"]
+            )
+
+        if None in (self.username, self.user_id, self.sec_uid):
+            User.parent.logger.error(
+                f"Failed to create User with data: {data}\nwhich has keys {data.keys()}"
+            )
+
+    def __update_id_sec_uid_username(self, id, sec_uid, username):
+        self.user_id = id
+        self.sec_uid = sec_uid
+        self.username = username
+
+    def __find_attributes(self) -> None:
+        # It is more efficient to check search first, since self.user_object() makes HTML request.
+        found = False
+        for u in self.parent.search.users(self.username):
+            if u.username == self.username:
+                found = True
+                self.__update_id_sec_uid_username(u.user_id, u.sec_uid, u.username)
+                break
+
+        if not found:
+            user_object = self.info()
+            self.__update_id_sec_uid_username(
+                user_object["id"], user_object["secUid"], user_object["uniqueId"]
+            )
+
+    def __repr__(self):
+        return self.__str__()
+
+    def __str__(self):
+        return f"TikTokApi.user(username='{self.username}', user_id='{self.user_id}', sec_uid='{self.sec_uid}')"
+
+    def __getattr__(self, name):
+        if name in ["as_dict"]:
+            self.as_dict = self.info()
+            self.__extract_from_data()
+            return self.__getattribute__(name)
+
+        raise AttributeError(f"{name} doesn't exist on TikTokApi.api.User")
+
+ +
+ +
+
+
+ #   + + + class + User: +
+ +
+ View Source +
class User:
+    """
+    A TikTok User.
+
+    Example Usage
+    ```py
+    user = api.user(username='therock')
+    # or
+    user_id = '5831967'
+    sec_uid = 'MS4wLjABAAAA-VASjiXTh7wDDyXvjk10VFhMWUAoxr8bgfO1kAL1-9s'
+    user = api.user(user_id=user_id, sec_uid=sec_uid)
+    ```
+
+    """
+
+    parent: ClassVar[TikTokApi]
+
+    user_id: str
+    """The user ID of the user."""
+    sec_uid: str
+    """The sec UID of the user."""
+    username: str
+    """The username of the user."""
+    as_dict: dict
+    """The raw data associated with this user."""
+
+    def __init__(
+        self,
+        username: Optional[str] = None,
+        user_id: Optional[str] = None,
+        sec_uid: Optional[str] = None,
+        data: Optional[str] = None,
+    ):
+        """
+        You must provide the username or (user_id and sec_uid) otherwise this
+        will not function correctly.
+        """
+        self.__update_id_sec_uid_username(user_id, sec_uid, username)
+        if data is not None:
+            self.as_dict = data
+            self.__extract_from_data()
+
+    def info(self, **kwargs):
+        """
+        Returns a dictionary of TikTok's User object
+
+        Example Usage
+        ```py
+        user_data = api.user(username='therock').info()
+        ```
+        """
+        return self.info_full(**kwargs)["user"]
+
+    def info_full(self, **kwargs) -> dict:
+        """
+        Returns a dictionary of information associated with this User.
+        Includes statistics about this user.
+
+        Example Usage
+        ```py
+        user_data = api.user(username='therock').info_full()
+        ```
+        """
+
+        # TODO: Find the one using only user_id & sec_uid
+        if not self.username:
+            raise TypeError(
+                "You must provide the username when creating this class to use this method."
+            )
+
+        quoted_username = quote(self.username)
+        r = requests.get(
+            "https://tiktok.com/@{}?lang=en".format(quoted_username),
+            headers={
+                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
+                "path": "/@{}".format(quoted_username),
+                "Accept-Encoding": "gzip, deflate",
+                "Connection": "keep-alive",
+                "User-Agent": self.parent.user_agent,
+            },
+            proxies=User.parent._format_proxy(kwargs.get("proxy", None)),
+            cookies=User.parent._get_cookies(**kwargs),
+            **User.parent.requests_extra_kwargs,
+        )
+
+        data = extract_tag_contents(r.text)
+        user = json.loads(data)
+
+        user_props = user["props"]["pageProps"]
+        if user_props["statusCode"] == 404:
+            raise TikTokNotFoundError(
+                "TikTok user with username {} does not exist".format(self.username)
+            )
+
+        return user_props["userInfo"]
+
+    def videos(self, count=30, cursor=0, **kwargs) -> Generator[Video, None, None]:
+        """
+        Returns a Generator yielding Video objects.
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+            - cursor (int): The unix epoch to get videos since. TODO: Check this is right
+
+        Example Usage
+        ```py
+        user = api.user(username='therock')
+        for video in user.videos(count=100):
+            print(video.id)
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = User.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        if not self.user_id and not self.sec_uid:
+            self.__find_attributes()
+
+        first = True
+        amount_yielded = 0
+
+        while amount_yielded < count:
+            query = {
+                "count": 30,
+                "id": self.user_id,
+                "cursor": cursor,
+                "type": 1,
+                "secUid": self.sec_uid,
+                "sourceType": 8,
+                "appId": 1233,
+                "region": region,
+                "priority_region": region,
+                "language": language,
+            }
+            path = "api/post/item_list/?{}&{}".format(
+                User.parent._add_url_params(), urlencode(query)
+            )
+
+            res = User.parent.get_data(path, send_tt_params=True, **kwargs)
+
+            videos = res.get("itemList", [])
+            amount_yielded += len(videos)
+            for video in videos:
+                yield self.parent.video(data=video)
+
+            if not res.get("hasMore", False) and not first:
+                User.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            cursor = res["cursor"]
+            first = False
+
+    def liked(
+        self, count: int = 30, cursor: int = 0, **kwargs
+    ) -> Generator[Video, None, None]:
+        """
+        Returns a dictionary listing TikToks that a given a user has liked.
+
+        **Note**: The user's likes must be **public** (which is not the default option)
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+            - cursor (int): The unix epoch to get videos since. TODO: Check this is right
+
+        Example Usage
+        ```py
+        for liked_video in api.user(username='public_likes'):
+            print(liked_video.id)
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        amount_yielded = 0
+        first = True
+
+        if self.user_id is None and self.sec_uid is None:
+            self.__find_attributes()
+
+        while amount_yielded < count:
+            query = {
+                "count": 30,
+                "id": self.user_id,
+                "type": 2,
+                "secUid": self.sec_uid,
+                "cursor": cursor,
+                "sourceType": 9,
+                "appId": 1233,
+                "region": region,
+                "priority_region": region,
+                "language": language,
+            }
+            path = "api/favorite/item_list/?{}&{}".format(
+                User.parent._add_url_params(), urlencode(query)
+            )
+
+            res = self.parent.get_data(path, **kwargs)
+
+            if "itemList" not in res.keys():
+                User.parent.logger.error("User's likes are most likely private")
+                return
+
+            videos = res.get("itemList", [])
+            amount_yielded += len(videos)
+            for video in videos:
+                amount_yielded += 1
+                yield self.parent.video(data=video)
+
+            if not res.get("hasMore", False) and not first:
+                User.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            cursor = res["cursor"]
+            first = False
+
+    def __extract_from_data(self):
+        data = self.as_dict
+        keys = data.keys()
+
+        if "user_info" in keys:
+            self.__update_id_sec_uid_username(
+                data["user_info"]["uid"],
+                data["user_info"]["sec_uid"],
+                data["user_info"]["unique_id"],
+            )
+        elif "uniqueId" in keys:
+            self.__update_id_sec_uid_username(
+                data["id"], data["secUid"], data["uniqueId"]
+            )
+
+        if None in (self.username, self.user_id, self.sec_uid):
+            User.parent.logger.error(
+                f"Failed to create User with data: {data}\nwhich has keys {data.keys()}"
+            )
+
+    def __update_id_sec_uid_username(self, id, sec_uid, username):
+        self.user_id = id
+        self.sec_uid = sec_uid
+        self.username = username
+
+    def __find_attributes(self) -> None:
+        # It is more efficient to check search first, since self.user_object() makes HTML request.
+        found = False
+        for u in self.parent.search.users(self.username):
+            if u.username == self.username:
+                found = True
+                self.__update_id_sec_uid_username(u.user_id, u.sec_uid, u.username)
+                break
+
+        if not found:
+            user_object = self.info()
+            self.__update_id_sec_uid_username(
+                user_object["id"], user_object["secUid"], user_object["uniqueId"]
+            )
+
+    def __repr__(self):
+        return self.__str__()
+
+    def __str__(self):
+        return f"TikTokApi.user(username='{self.username}', user_id='{self.user_id}', sec_uid='{self.sec_uid}')"
+
+    def __getattr__(self, name):
+        if name in ["as_dict"]:
+            self.as_dict = self.info()
+            self.__extract_from_data()
+            return self.__getattribute__(name)
+
+        raise AttributeError(f"{name} doesn't exist on TikTokApi.api.User")
+
+ +
+ +

A TikTok User.

+ +

Example Usage

+ +
user = api.user(username='therock')
+# or
+user_id = '5831967'
+sec_uid = 'MS4wLjABAAAA-VASjiXTh7wDDyXvjk10VFhMWUAoxr8bgfO1kAL1-9s'
+user = api.user(user_id=user_id, sec_uid=sec_uid)
+
+
+ + +
+
#   + + + User( + username: Optional[str] = None, + user_id: Optional[str] = None, + sec_uid: Optional[str] = None, + data: Optional[str] = None +) +
+ +
+ View Source +
    def __init__(
+        self,
+        username: Optional[str] = None,
+        user_id: Optional[str] = None,
+        sec_uid: Optional[str] = None,
+        data: Optional[str] = None,
+    ):
+        """
+        You must provide the username or (user_id and sec_uid) otherwise this
+        will not function correctly.
+        """
+        self.__update_id_sec_uid_username(user_id, sec_uid, username)
+        if data is not None:
+            self.as_dict = data
+            self.__extract_from_data()
+
+ +
+ +

You must provide the username or (user_id and sec_uid) otherwise this +will not function correctly.

+
+ + +
+
+
#   + + user_id: str +
+ +

The user ID of the user.

+
+ + +
+
+
#   + + sec_uid: str +
+ +

The sec UID of the user.

+
+ + +
+
+
#   + + username: str +
+ +

The username of the user.

+
+ + +
+
+
#   + + as_dict: dict +
+ +

The raw data associated with this user.

+
+ + +
+
+
#   + + + def + info(self, **kwargs): +
+ +
+ View Source +
    def info(self, **kwargs):
+        """
+        Returns a dictionary of TikTok's User object
+
+        Example Usage
+        ```py
+        user_data = api.user(username='therock').info()
+        ```
+        """
+        return self.info_full(**kwargs)["user"]
+
+ +
+ +

Returns a dictionary of TikTok's User object

+ +

Example Usage

+ +
user_data = api.user(username='therock').info()
+
+
+ + +
+
+
#   + + + def + info_full(self, **kwargs) -> dict: +
+ +
+ View Source +
    def info_full(self, **kwargs) -> dict:
+        """
+        Returns a dictionary of information associated with this User.
+        Includes statistics about this user.
+
+        Example Usage
+        ```py
+        user_data = api.user(username='therock').info_full()
+        ```
+        """
+
+        # TODO: Find the one using only user_id & sec_uid
+        if not self.username:
+            raise TypeError(
+                "You must provide the username when creating this class to use this method."
+            )
+
+        quoted_username = quote(self.username)
+        r = requests.get(
+            "https://tiktok.com/@{}?lang=en".format(quoted_username),
+            headers={
+                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
+                "path": "/@{}".format(quoted_username),
+                "Accept-Encoding": "gzip, deflate",
+                "Connection": "keep-alive",
+                "User-Agent": self.parent.user_agent,
+            },
+            proxies=User.parent._format_proxy(kwargs.get("proxy", None)),
+            cookies=User.parent._get_cookies(**kwargs),
+            **User.parent.requests_extra_kwargs,
+        )
+
+        data = extract_tag_contents(r.text)
+        user = json.loads(data)
+
+        user_props = user["props"]["pageProps"]
+        if user_props["statusCode"] == 404:
+            raise TikTokNotFoundError(
+                "TikTok user with username {} does not exist".format(self.username)
+            )
+
+        return user_props["userInfo"]
+
+ +
+ +

Returns a dictionary of information associated with this User. +Includes statistics about this user.

+ +

Example Usage

+ +
user_data = api.user(username='therock').info_full()
+
+
+ + +
+
+
#   + + + def + videos( + self, + count=30, + cursor=0, + **kwargs +) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]: +
+ +
+ View Source +
    def videos(self, count=30, cursor=0, **kwargs) -> Generator[Video, None, None]:
+        """
+        Returns a Generator yielding Video objects.
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+            - cursor (int): The unix epoch to get videos since. TODO: Check this is right
+
+        Example Usage
+        ```py
+        user = api.user(username='therock')
+        for video in user.videos(count=100):
+            print(video.id)
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = User.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        if not self.user_id and not self.sec_uid:
+            self.__find_attributes()
+
+        first = True
+        amount_yielded = 0
+
+        while amount_yielded < count:
+            query = {
+                "count": 30,
+                "id": self.user_id,
+                "cursor": cursor,
+                "type": 1,
+                "secUid": self.sec_uid,
+                "sourceType": 8,
+                "appId": 1233,
+                "region": region,
+                "priority_region": region,
+                "language": language,
+            }
+            path = "api/post/item_list/?{}&{}".format(
+                User.parent._add_url_params(), urlencode(query)
+            )
+
+            res = User.parent.get_data(path, send_tt_params=True, **kwargs)
+
+            videos = res.get("itemList", [])
+            amount_yielded += len(videos)
+            for video in videos:
+                yield self.parent.video(data=video)
+
+            if not res.get("hasMore", False) and not first:
+                User.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            cursor = res["cursor"]
+            first = False
+
+ +
+ +

Returns a Generator yielding Video objects.

+ +
    +
  • Parameters: +
      +
    • count (int): The amount of videos you want returned.
    • +
    • cursor (int): The unix epoch to get videos since. TODO: Check this is right
    • +
  • +
+ +

Example Usage

+ +
user = api.user(username='therock')
+for video in user.videos(count=100):
+    print(video.id)
+
+
+ + +
+
+
#   + + + def + liked( + self, + count: int = 30, + cursor: int = 0, + **kwargs +) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]: +
+ +
+ View Source +
    def liked(
+        self, count: int = 30, cursor: int = 0, **kwargs
+    ) -> Generator[Video, None, None]:
+        """
+        Returns a dictionary listing TikToks that a given a user has liked.
+
+        **Note**: The user's likes must be **public** (which is not the default option)
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+            - cursor (int): The unix epoch to get videos since. TODO: Check this is right
+
+        Example Usage
+        ```py
+        for liked_video in api.user(username='public_likes'):
+            print(liked_video.id)
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        amount_yielded = 0
+        first = True
+
+        if self.user_id is None and self.sec_uid is None:
+            self.__find_attributes()
+
+        while amount_yielded < count:
+            query = {
+                "count": 30,
+                "id": self.user_id,
+                "type": 2,
+                "secUid": self.sec_uid,
+                "cursor": cursor,
+                "sourceType": 9,
+                "appId": 1233,
+                "region": region,
+                "priority_region": region,
+                "language": language,
+            }
+            path = "api/favorite/item_list/?{}&{}".format(
+                User.parent._add_url_params(), urlencode(query)
+            )
+
+            res = self.parent.get_data(path, **kwargs)
+
+            if "itemList" not in res.keys():
+                User.parent.logger.error("User's likes are most likely private")
+                return
+
+            videos = res.get("itemList", [])
+            amount_yielded += len(videos)
+            for video in videos:
+                amount_yielded += 1
+                yield self.parent.video(data=video)
+
+            if not res.get("hasMore", False) and not first:
+                User.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            cursor = res["cursor"]
+            first = False
+
+ +
+ +

Returns a dictionary listing TikToks that a given a user has liked.

+ +

Note: The user's likes must be public (which is not the default option)

+ +
    +
  • Parameters: +
      +
    • count (int): The amount of videos you want returned.
    • +
    • cursor (int): The unix epoch to get videos since. TODO: Check this is right
    • +
  • +
+ +

Example Usage

+ +
for liked_video in api.user(username='public_likes'):
+    print(liked_video.id)
+
+
+ + +
+
+
+ + \ No newline at end of file diff --git a/docs/TikTokApi/api/video.html b/docs/TikTokApi/api/video.html new file mode 100644 index 00000000..77c4f996 --- /dev/null +++ b/docs/TikTokApi/api/video.html @@ -0,0 +1,826 @@ + + + + + + + TikTokApi.api.video API documentation + + + + + + + + +
+
+

+TikTokApi.api.video

+ + +
+ View Source +
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
+
+if TYPE_CHECKING:
+    from ..tiktok import TikTokApi
+    from .user import User
+    from .sound import Sound
+    from .hashtag import Hashtag
+
+
+class Video:
+    """
+    A TikTok Video class
+
+    Example Usage
+    ```py
+    video = api.video(id='7041997751718137094')
+    ```
+    """
+
+    parent: ClassVar[TikTokApi]
+
+    id: str
+    """TikTok's ID of the Video"""
+    author: Optional[User]
+    """The User who created the Video"""
+    sound: Optional[Sound]
+    """The Sound that is associated with the Video"""
+    hashtags: Optional[list[Hashtag]]
+    """A List of Hashtags on the Video"""
+    as_dict: dict
+    """The raw data associated with this Video."""
+
+    def __init__(
+        self,
+        id: Optional[str] = None,
+        url: Optional[str] = None,
+        data: Optional[dict] = None,
+    ):
+        """
+        You must provide the id or a valid url, else this will fail.
+        """
+        self.id = id
+        if data is not None:
+            self.as_dict = data
+            self.__extract_from_data()
+        elif url is not None:
+            self.id = extract_video_id_from_url(url)
+
+        if self.id is None:
+            raise TypeError("You must provide id or url parameter.")
+
+    def info(self, **kwargs) -> dict:
+        """
+        Returns a dictionary of TikTok's Video object.
+
+        Example Usage
+        ```py
+        video_data = api.video(id='7041997751718137094').info()
+        ```
+        """
+        return self.info_full(**kwargs)["itemInfo"]["itemStruct"]
+
+    def info_full(self, **kwargs) -> dict:
+        """
+        Returns a dictionary of all data associated with a TikTok Video.
+
+        Example Usage
+        ```py
+        video_data = api.video(id='7041997751718137094').info_full()
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        device_id = kwargs.get("custom_device_id", None)
+        query = {
+            "itemId": self.id,
+        }
+        path = "api/item/detail/?{}&{}".format(
+            self.parent._add_url_params(), urlencode(query)
+        )
+
+        return self.parent.get_data(path, **kwargs)
+
+    def bytes(self, **kwargs) -> bytes:
+        """
+        Returns the bytes of a TikTok Video.
+
+        Example Usage
+        ```py
+        video_bytes = api.video(id='7041997751718137094').bytes()
+
+        # Saving The Video
+        with open('saved_video.mp4', 'wb') as output:
+            output.write(video_bytes)
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        video_data = self.info(**kwargs)
+        download_url = video_data["video"]["playAddr"]
+
+        return self.parent.get_bytes(url=download_url, **kwargs)
+
+    def __extract_from_data(self) -> None:
+        data = self.as_dict
+        keys = data.keys()
+
+        if "author" in keys:
+            self.id = data["id"]
+            self.author = self.parent.user(data=data["author"])
+            self.sound = self.parent.sound(data=data["music"])
+
+            self.hashtags = [
+                self.parent.hashtag(data=hashtag)
+                for hashtag in data.get("challenges", [])
+            ]
+
+        if self.id is None:
+            Video.parent.logger.error(
+                f"Failed to create Video with data: {data}\nwhich has keys {data.keys()}"
+            )
+
+    def __repr__(self):
+        return self.__str__()
+
+    def __str__(self):
+        return f"TikTokApi.video(id='{self.id}')"
+
+    def __getattr__(self, name):
+        # Handle author, sound, hashtags, as_dict
+        if name in ["author", "sound", "hashtags", "as_dict"]:
+            self.as_dict = self.info()
+            self.__extract_from_data()
+            return self.__getattribute__(name)
+
+        raise AttributeError(f"{name} doesn't exist on TikTokApi.api.Video")
+
+ +
+ +
+
+
+ #   + + + class + Video: +
+ +
+ View Source +
class Video:
+    """
+    A TikTok Video class
+
+    Example Usage
+    ```py
+    video = api.video(id='7041997751718137094')
+    ```
+    """
+
+    parent: ClassVar[TikTokApi]
+
+    id: str
+    """TikTok's ID of the Video"""
+    author: Optional[User]
+    """The User who created the Video"""
+    sound: Optional[Sound]
+    """The Sound that is associated with the Video"""
+    hashtags: Optional[list[Hashtag]]
+    """A List of Hashtags on the Video"""
+    as_dict: dict
+    """The raw data associated with this Video."""
+
+    def __init__(
+        self,
+        id: Optional[str] = None,
+        url: Optional[str] = None,
+        data: Optional[dict] = None,
+    ):
+        """
+        You must provide the id or a valid url, else this will fail.
+        """
+        self.id = id
+        if data is not None:
+            self.as_dict = data
+            self.__extract_from_data()
+        elif url is not None:
+            self.id = extract_video_id_from_url(url)
+
+        if self.id is None:
+            raise TypeError("You must provide id or url parameter.")
+
+    def info(self, **kwargs) -> dict:
+        """
+        Returns a dictionary of TikTok's Video object.
+
+        Example Usage
+        ```py
+        video_data = api.video(id='7041997751718137094').info()
+        ```
+        """
+        return self.info_full(**kwargs)["itemInfo"]["itemStruct"]
+
+    def info_full(self, **kwargs) -> dict:
+        """
+        Returns a dictionary of all data associated with a TikTok Video.
+
+        Example Usage
+        ```py
+        video_data = api.video(id='7041997751718137094').info_full()
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        device_id = kwargs.get("custom_device_id", None)
+        query = {
+            "itemId": self.id,
+        }
+        path = "api/item/detail/?{}&{}".format(
+            self.parent._add_url_params(), urlencode(query)
+        )
+
+        return self.parent.get_data(path, **kwargs)
+
+    def bytes(self, **kwargs) -> bytes:
+        """
+        Returns the bytes of a TikTok Video.
+
+        Example Usage
+        ```py
+        video_bytes = api.video(id='7041997751718137094').bytes()
+
+        # Saving The Video
+        with open('saved_video.mp4', 'wb') as output:
+            output.write(video_bytes)
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        video_data = self.info(**kwargs)
+        download_url = video_data["video"]["playAddr"]
+
+        return self.parent.get_bytes(url=download_url, **kwargs)
+
+    def __extract_from_data(self) -> None:
+        data = self.as_dict
+        keys = data.keys()
+
+        if "author" in keys:
+            self.id = data["id"]
+            self.author = self.parent.user(data=data["author"])
+            self.sound = self.parent.sound(data=data["music"])
+
+            self.hashtags = [
+                self.parent.hashtag(data=hashtag)
+                for hashtag in data.get("challenges", [])
+            ]
+
+        if self.id is None:
+            Video.parent.logger.error(
+                f"Failed to create Video with data: {data}\nwhich has keys {data.keys()}"
+            )
+
+    def __repr__(self):
+        return self.__str__()
+
+    def __str__(self):
+        return f"TikTokApi.video(id='{self.id}')"
+
+    def __getattr__(self, name):
+        # Handle author, sound, hashtags, as_dict
+        if name in ["author", "sound", "hashtags", "as_dict"]:
+            self.as_dict = self.info()
+            self.__extract_from_data()
+            return self.__getattribute__(name)
+
+        raise AttributeError(f"{name} doesn't exist on TikTokApi.api.Video")
+
+ +
+ +

A TikTok Video class

+ +

Example Usage

+ +
video = api.video(id='7041997751718137094')
+
+
+ + +
+
#   + + + Video( + id: Optional[str] = None, + url: Optional[str] = None, + data: Optional[dict] = None +) +
+ +
+ View Source +
    def __init__(
+        self,
+        id: Optional[str] = None,
+        url: Optional[str] = None,
+        data: Optional[dict] = None,
+    ):
+        """
+        You must provide the id or a valid url, else this will fail.
+        """
+        self.id = id
+        if data is not None:
+            self.as_dict = data
+            self.__extract_from_data()
+        elif url is not None:
+            self.id = extract_video_id_from_url(url)
+
+        if self.id is None:
+            raise TypeError("You must provide id or url parameter.")
+
+ +
+ +

You must provide the id or a valid url, else this will fail.

+
+ + +
+
+
#   + + id: str +
+ +

TikTok's ID of the Video

+
+ + +
+
+
#   + + author: Optional[TikTokApi.api.user.User] +
+ +

The User who created the Video

+
+ + +
+
+
#   + + sound: Optional[TikTokApi.api.sound.Sound] +
+ +

The Sound that is associated with the Video

+
+ + +
+
+
#   + + hashtags: Optional[list[TikTokApi.api.hashtag.Hashtag]] +
+ +

A List of Hashtags on the Video

+
+ + +
+
+
#   + + as_dict: dict +
+ +

The raw data associated with this Video.

+
+ + +
+
+
#   + + + def + info(self, **kwargs) -> dict: +
+ +
+ View Source +
    def info(self, **kwargs) -> dict:
+        """
+        Returns a dictionary of TikTok's Video object.
+
+        Example Usage
+        ```py
+        video_data = api.video(id='7041997751718137094').info()
+        ```
+        """
+        return self.info_full(**kwargs)["itemInfo"]["itemStruct"]
+
+ +
+ +

Returns a dictionary of TikTok's Video object.

+ +

Example Usage

+ +
video_data = api.video(id='7041997751718137094').info()
+
+
+ + +
+
+
#   + + + def + info_full(self, **kwargs) -> dict: +
+ +
+ View Source +
    def info_full(self, **kwargs) -> dict:
+        """
+        Returns a dictionary of all data associated with a TikTok Video.
+
+        Example Usage
+        ```py
+        video_data = api.video(id='7041997751718137094').info_full()
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        device_id = kwargs.get("custom_device_id", None)
+        query = {
+            "itemId": self.id,
+        }
+        path = "api/item/detail/?{}&{}".format(
+            self.parent._add_url_params(), urlencode(query)
+        )
+
+        return self.parent.get_data(path, **kwargs)
+
+ +
+ +

Returns a dictionary of all data associated with a TikTok Video.

+ +

Example Usage

+ +
video_data = api.video(id='7041997751718137094').info_full()
+
+
+ + +
+
+
#   + + + def + bytes(self, **kwargs) -> bytes: +
+ +
+ View Source +
    def bytes(self, **kwargs) -> bytes:
+        """
+        Returns the bytes of a TikTok Video.
+
+        Example Usage
+        ```py
+        video_bytes = api.video(id='7041997751718137094').bytes()
+
+        # Saving The Video
+        with open('saved_video.mp4', 'wb') as output:
+            output.write(video_bytes)
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        video_data = self.info(**kwargs)
+        download_url = video_data["video"]["playAddr"]
+
+        return self.parent.get_bytes(url=download_url, **kwargs)
+
+ +
+ +

Returns the bytes of a TikTok Video.

+ +

Example Usage

+ +
video_bytes = api.video(id='7041997751718137094').bytes()
+
+# Saving The Video
+with open('saved_video.mp4', 'wb') as output:
+    output.write(video_bytes)
+
+
+ + +
+
+
+ + \ No newline at end of file diff --git a/docs/TikTokApi/browser_utilities.html b/docs/TikTokApi/browser_utilities.html new file mode 100644 index 00000000..ae4a3a23 --- /dev/null +++ b/docs/TikTokApi/browser_utilities.html @@ -0,0 +1,239 @@ + + + + + + + TikTokApi.browser_utilities API documentation + + + + + + + + +
+
+

+TikTokApi.browser_utilities

+ + +
+ View Source +
from .browser_interface import BrowserInterface
+
+ +
+ +
+
+ + \ No newline at end of file diff --git a/docs/TikTokApi/browser_utilities/browser.html b/docs/TikTokApi/browser_utilities/browser.html new file mode 100644 index 00000000..15eed1b6 --- /dev/null +++ b/docs/TikTokApi/browser_utilities/browser.html @@ -0,0 +1,1062 @@ + + + + + + + TikTokApi.browser_utilities.browser API documentation + + + + + + + + +
+
+

+TikTokApi.browser_utilities.browser

+ + +
+ View Source +
import random
+import time
+import string
+import requests
+import logging
+import time
+import random
+import json
+import re
+from .browser_interface import BrowserInterface
+from urllib.parse import splitquery, parse_qs, parse_qsl
+
+from ..utilities import LOGGER_NAME
+from .get_acrawler import _get_acrawler, _get_tt_params_script
+from playwright.sync_api import sync_playwright
+
+playwright = None
+
+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,
+        **kwargs,
+    ):
+        self.kwargs = kwargs
+        self.debug = kwargs.get("debug", False)
+        self.proxy = kwargs.get("proxy", None)
+        self.api_url = kwargs.get("api_url", None)
+        self.referrer = kwargs.get("referer", "https://www.tiktok.com/")
+        self.language = kwargs.get("language", "en")
+        self.executablePath = kwargs.get("executablePath", None)
+        self.device_id = kwargs.get("custom_device_id", None)
+
+        args = kwargs.get("browser_args", [])
+        options = kwargs.get("browser_options", {})
+
+        if len(args) == 0:
+            self.args = []
+        else:
+            self.args = args
+
+        self.options = {
+            "headless": True,
+            "handle_sigint": True,
+            "handle_sigterm": True,
+            "handle_sighup": True,
+        }
+
+        if self.proxy is not None:
+            if "@" in self.proxy:
+                server_prefix = self.proxy.split("://")[0]
+                address = self.proxy.split("@")[1]
+                self.options["proxy"] = {
+                    "server": server_prefix + "://" + address,
+                    "username": self.proxy.split("://")[1].split(":")[0],
+                    "password": self.proxy.split("://")[1].split("@")[0].split(":")[1],
+                }
+            else:
+                self.options["proxy"] = {"server": self.proxy}
+
+        self.options.update(options)
+
+        if self.executablePath is not None:
+            self.options["executablePath"] = self.executablePath
+
+        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
+
+        context = self._create_context(set_useragent=True)
+        page = context.new_page()
+        self.get_params(page)
+        context.close()
+
+    def get_params(self, page) -> None:
+        self.browser_language = self.kwargs.get(
+            "browser_language",
+            page.evaluate("""() => { return navigator.language; }"""),
+        )
+        self.browser_version = page.evaluate(
+            """() => { return window.navigator.appVersion; }"""
+        )
+
+        if len(self.browser_language.split("-")) == 0:
+            self.region = self.kwargs.get("region", "US")
+            self.language = self.kwargs.get("language", "en")
+        elif len(self.browser_language.split("-")) == 1:
+            self.region = self.kwargs.get("region", "US")
+            self.language = self.browser_language.split("-")[0]
+        else:
+            self.region = self.kwargs.get("region", self.browser_language.split("-")[1])
+            self.language = self.kwargs.get(
+                "language", self.browser_language.split("-")[0]
+            )
+
+        self.timezone_name = self.kwargs.get(
+            "timezone_name",
+            page.evaluate(
+                """() => { return Intl.DateTimeFormat().resolvedOptions().timeZone; }"""
+            ),
+        )
+        self.width = page.evaluate("""() => { return screen.width; }""")
+        self.height = page.evaluate("""() => { return screen.height; }""")
+
+    def _create_context(self, set_useragent=False):
+        iphone = playwright.devices["iPhone 11 Pro"]
+        iphone["viewport"] = {
+            "width": random.randint(320, 1920),
+            "height": random.randint(320, 1920),
+        }
+        iphone["device_scale_factor"] = random.randint(1, 3)
+        iphone["is_mobile"] = random.randint(1, 2) == 1
+        iphone["has_touch"] = random.randint(1, 2) == 1
+
+        iphone["bypass_csp"] = True
+
+        context = self.browser.new_context(**iphone)
+        if set_useragent:
+            self.user_agent = iphone["user_agent"]
+
+        return context
+
+    def _base36encode(self, number, alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
+        """Converts an integer to a base36 string."""
+        base36 = ""
+        sign = ""
+
+        if number < 0:
+            sign = "-"
+            number = -number
+
+        if 0 <= number < len(alphabet):
+            return sign + alphabet[number]
+
+        while number != 0:
+            number, i = divmod(number, len(alphabet))
+            base36 = alphabet[i] + base36
+
+        return sign + base36
+
+    def gen_verifyFp(self):
+        chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[:]
+        chars_len = len(chars)
+        scenario_title = self._base36encode(int(time.time() * 1000))
+        uuid = [0] * 36
+        uuid[8] = "_"
+        uuid[13] = "_"
+        uuid[18] = "_"
+        uuid[23] = "_"
+        uuid[14] = "4"
+
+        for i in range(36):
+            if uuid[i] != 0:
+                continue
+            r = int(random.random() * chars_len)
+            uuid[i] = chars[int((3 & r) | 8 if i == 19 else r)]
+
+        return f'verify_{scenario_title.lower()}_{"".join(uuid)}'
+
+    def sign_url(self, url, calc_tt_params=False, **kwargs):
+        def process(route):
+            route.abort()
+
+        tt_params = None
+        context = self._create_context()
+        page = context.new_page()
+
+        if calc_tt_params:
+            page.route(re.compile(r"(\.png)|(\.jpeg)|(\.mp4)|(x-expire)"), process)
+            page.goto(
+                kwargs.get("default_url", "https://www.tiktok.com/@redbull"),
+                wait_until="load",
+            )
+
+        verifyFp = "".join(
+            random.choice(
+                string.ascii_lowercase + string.ascii_uppercase + string.digits
+            )
+            for i in range(16)
+        )
+        if kwargs.get("gen_new_verifyFp", False):
+            verifyFp = self.gen_verifyFp()
+        else:
+            verifyFp = kwargs.get(
+                "custom_verify_fp",
+                "verify_khgp4f49_V12d4mRX_MdCO_4Wzt_Ar0k_z4RCQC9pUDpX",
+            )
+
+        if kwargs.get("custom_device_id") is not None:
+            device_id = kwargs.get("custom_device_id", None)
+        elif self.device_id is None:
+            device_id = str(random.randint(10000, 999999999))
+        else:
+            device_id = self.device_id
+
+        url = "{}&verifyFp={}&device_id={}".format(url, verifyFp, device_id)
+
+        page.add_script_tag(content=_get_acrawler())
+        evaluatedPage = page.evaluate(
+            '''() => {
+            var url = "'''
+            + url
+            + """"
+            var token = window.byted_acrawler.sign({url: url});
+            
+            return token;
+            }"""
+        )
+
+        url = "{}&_signature={}".format(url, evaluatedPage)
+
+        if calc_tt_params:
+            page.add_script_tag(content=_get_tt_params_script())
+
+            tt_params = page.evaluate(
+                """() => {
+                    return window.genXTTParams("""
+                + json.dumps(dict(parse_qsl(splitquery(url)[1])))
+                + """);
+            
+                }"""
+            )
+
+        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()
+
+    def find_redirect(self, url):
+        self.page.goto(url, {"waitUntil": "load"})
+        self.redirect_url = self.page.url
+
+    def __format_proxy(self, proxy):
+        if proxy is not None:
+            return {"http": proxy, "https": proxy}
+        else:
+            return None
+
+    def __get_js(self):
+        return requests.get(
+            "https://sf16-muse-va.ibytedtos.com/obj/rc-web-sdk-gcs/acrawler.js",
+            proxies=self.__format_proxy(self.proxy),
+        ).text
+
+ +
+ +
+
+
#   + + + def + get_playwright(): +
+ +
+ View Source +
def get_playwright():
+    global playwright
+    if playwright is None:
+        try:
+            playwright = sync_playwright().start()
+        except Exception as e:
+            raise e
+
+    return playwright
+
+ +
+ + + +
+
+ + +
+ View Source +
class browser(BrowserInterface):
+    def __init__(
+        self,
+        **kwargs,
+    ):
+        self.kwargs = kwargs
+        self.debug = kwargs.get("debug", False)
+        self.proxy = kwargs.get("proxy", None)
+        self.api_url = kwargs.get("api_url", None)
+        self.referrer = kwargs.get("referer", "https://www.tiktok.com/")
+        self.language = kwargs.get("language", "en")
+        self.executablePath = kwargs.get("executablePath", None)
+        self.device_id = kwargs.get("custom_device_id", None)
+
+        args = kwargs.get("browser_args", [])
+        options = kwargs.get("browser_options", {})
+
+        if len(args) == 0:
+            self.args = []
+        else:
+            self.args = args
+
+        self.options = {
+            "headless": True,
+            "handle_sigint": True,
+            "handle_sigterm": True,
+            "handle_sighup": True,
+        }
+
+        if self.proxy is not None:
+            if "@" in self.proxy:
+                server_prefix = self.proxy.split("://")[0]
+                address = self.proxy.split("@")[1]
+                self.options["proxy"] = {
+                    "server": server_prefix + "://" + address,
+                    "username": self.proxy.split("://")[1].split(":")[0],
+                    "password": self.proxy.split("://")[1].split("@")[0].split(":")[1],
+                }
+            else:
+                self.options["proxy"] = {"server": self.proxy}
+
+        self.options.update(options)
+
+        if self.executablePath is not None:
+            self.options["executablePath"] = self.executablePath
+
+        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
+
+        context = self._create_context(set_useragent=True)
+        page = context.new_page()
+        self.get_params(page)
+        context.close()
+
+    def get_params(self, page) -> None:
+        self.browser_language = self.kwargs.get(
+            "browser_language",
+            page.evaluate("""() => { return navigator.language; }"""),
+        )
+        self.browser_version = page.evaluate(
+            """() => { return window.navigator.appVersion; }"""
+        )
+
+        if len(self.browser_language.split("-")) == 0:
+            self.region = self.kwargs.get("region", "US")
+            self.language = self.kwargs.get("language", "en")
+        elif len(self.browser_language.split("-")) == 1:
+            self.region = self.kwargs.get("region", "US")
+            self.language = self.browser_language.split("-")[0]
+        else:
+            self.region = self.kwargs.get("region", self.browser_language.split("-")[1])
+            self.language = self.kwargs.get(
+                "language", self.browser_language.split("-")[0]
+            )
+
+        self.timezone_name = self.kwargs.get(
+            "timezone_name",
+            page.evaluate(
+                """() => { return Intl.DateTimeFormat().resolvedOptions().timeZone; }"""
+            ),
+        )
+        self.width = page.evaluate("""() => { return screen.width; }""")
+        self.height = page.evaluate("""() => { return screen.height; }""")
+
+    def _create_context(self, set_useragent=False):
+        iphone = playwright.devices["iPhone 11 Pro"]
+        iphone["viewport"] = {
+            "width": random.randint(320, 1920),
+            "height": random.randint(320, 1920),
+        }
+        iphone["device_scale_factor"] = random.randint(1, 3)
+        iphone["is_mobile"] = random.randint(1, 2) == 1
+        iphone["has_touch"] = random.randint(1, 2) == 1
+
+        iphone["bypass_csp"] = True
+
+        context = self.browser.new_context(**iphone)
+        if set_useragent:
+            self.user_agent = iphone["user_agent"]
+
+        return context
+
+    def _base36encode(self, number, alphabet="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"):
+        """Converts an integer to a base36 string."""
+        base36 = ""
+        sign = ""
+
+        if number < 0:
+            sign = "-"
+            number = -number
+
+        if 0 <= number < len(alphabet):
+            return sign + alphabet[number]
+
+        while number != 0:
+            number, i = divmod(number, len(alphabet))
+            base36 = alphabet[i] + base36
+
+        return sign + base36
+
+    def gen_verifyFp(self):
+        chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[:]
+        chars_len = len(chars)
+        scenario_title = self._base36encode(int(time.time() * 1000))
+        uuid = [0] * 36
+        uuid[8] = "_"
+        uuid[13] = "_"
+        uuid[18] = "_"
+        uuid[23] = "_"
+        uuid[14] = "4"
+
+        for i in range(36):
+            if uuid[i] != 0:
+                continue
+            r = int(random.random() * chars_len)
+            uuid[i] = chars[int((3 & r) | 8 if i == 19 else r)]
+
+        return f'verify_{scenario_title.lower()}_{"".join(uuid)}'
+
+    def sign_url(self, url, calc_tt_params=False, **kwargs):
+        def process(route):
+            route.abort()
+
+        tt_params = None
+        context = self._create_context()
+        page = context.new_page()
+
+        if calc_tt_params:
+            page.route(re.compile(r"(\.png)|(\.jpeg)|(\.mp4)|(x-expire)"), process)
+            page.goto(
+                kwargs.get("default_url", "https://www.tiktok.com/@redbull"),
+                wait_until="load",
+            )
+
+        verifyFp = "".join(
+            random.choice(
+                string.ascii_lowercase + string.ascii_uppercase + string.digits
+            )
+            for i in range(16)
+        )
+        if kwargs.get("gen_new_verifyFp", False):
+            verifyFp = self.gen_verifyFp()
+        else:
+            verifyFp = kwargs.get(
+                "custom_verify_fp",
+                "verify_khgp4f49_V12d4mRX_MdCO_4Wzt_Ar0k_z4RCQC9pUDpX",
+            )
+
+        if kwargs.get("custom_device_id") is not None:
+            device_id = kwargs.get("custom_device_id", None)
+        elif self.device_id is None:
+            device_id = str(random.randint(10000, 999999999))
+        else:
+            device_id = self.device_id
+
+        url = "{}&verifyFp={}&device_id={}".format(url, verifyFp, device_id)
+
+        page.add_script_tag(content=_get_acrawler())
+        evaluatedPage = page.evaluate(
+            '''() => {
+            var url = "'''
+            + url
+            + """"
+            var token = window.byted_acrawler.sign({url: url});
+            
+            return token;
+            }"""
+        )
+
+        url = "{}&_signature={}".format(url, evaluatedPage)
+
+        if calc_tt_params:
+            page.add_script_tag(content=_get_tt_params_script())
+
+            tt_params = page.evaluate(
+                """() => {
+                    return window.genXTTParams("""
+                + json.dumps(dict(parse_qsl(splitquery(url)[1])))
+                + """);
+            
+                }"""
+            )
+
+        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()
+
+    def find_redirect(self, url):
+        self.page.goto(url, {"waitUntil": "load"})
+        self.redirect_url = self.page.url
+
+    def __format_proxy(self, proxy):
+        if proxy is not None:
+            return {"http": proxy, "https": proxy}
+        else:
+            return None
+
+    def __get_js(self):
+        return requests.get(
+            "https://sf16-muse-va.ibytedtos.com/obj/rc-web-sdk-gcs/acrawler.js",
+            proxies=self.__format_proxy(self.proxy),
+        ).text
+
+ +
+ +

Helper class that provides a standard way to create an ABC using +inheritance.

+
+ + +
+
#   + + + browser(**kwargs) +
+ +
+ View Source +
    def __init__(
+        self,
+        **kwargs,
+    ):
+        self.kwargs = kwargs
+        self.debug = kwargs.get("debug", False)
+        self.proxy = kwargs.get("proxy", None)
+        self.api_url = kwargs.get("api_url", None)
+        self.referrer = kwargs.get("referer", "https://www.tiktok.com/")
+        self.language = kwargs.get("language", "en")
+        self.executablePath = kwargs.get("executablePath", None)
+        self.device_id = kwargs.get("custom_device_id", None)
+
+        args = kwargs.get("browser_args", [])
+        options = kwargs.get("browser_options", {})
+
+        if len(args) == 0:
+            self.args = []
+        else:
+            self.args = args
+
+        self.options = {
+            "headless": True,
+            "handle_sigint": True,
+            "handle_sigterm": True,
+            "handle_sighup": True,
+        }
+
+        if self.proxy is not None:
+            if "@" in self.proxy:
+                server_prefix = self.proxy.split("://")[0]
+                address = self.proxy.split("@")[1]
+                self.options["proxy"] = {
+                    "server": server_prefix + "://" + address,
+                    "username": self.proxy.split("://")[1].split(":")[0],
+                    "password": self.proxy.split("://")[1].split("@")[0].split(":")[1],
+                }
+            else:
+                self.options["proxy"] = {"server": self.proxy}
+
+        self.options.update(options)
+
+        if self.executablePath is not None:
+            self.options["executablePath"] = self.executablePath
+
+        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
+
+        context = self._create_context(set_useragent=True)
+        page = context.new_page()
+        self.get_params(page)
+        context.close()
+
+ +
+ + + +
+
+
#   + + + def + get_params(self, page) -> None: +
+ +
+ View Source +
    def get_params(self, page) -> None:
+        self.browser_language = self.kwargs.get(
+            "browser_language",
+            page.evaluate("""() => { return navigator.language; }"""),
+        )
+        self.browser_version = page.evaluate(
+            """() => { return window.navigator.appVersion; }"""
+        )
+
+        if len(self.browser_language.split("-")) == 0:
+            self.region = self.kwargs.get("region", "US")
+            self.language = self.kwargs.get("language", "en")
+        elif len(self.browser_language.split("-")) == 1:
+            self.region = self.kwargs.get("region", "US")
+            self.language = self.browser_language.split("-")[0]
+        else:
+            self.region = self.kwargs.get("region", self.browser_language.split("-")[1])
+            self.language = self.kwargs.get(
+                "language", self.browser_language.split("-")[0]
+            )
+
+        self.timezone_name = self.kwargs.get(
+            "timezone_name",
+            page.evaluate(
+                """() => { return Intl.DateTimeFormat().resolvedOptions().timeZone; }"""
+            ),
+        )
+        self.width = page.evaluate("""() => { return screen.width; }""")
+        self.height = page.evaluate("""() => { return screen.height; }""")
+
+ +
+ + + +
+
+
#   + + + def + gen_verifyFp(self): +
+ +
+ View Source +
    def gen_verifyFp(self):
+        chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"[:]
+        chars_len = len(chars)
+        scenario_title = self._base36encode(int(time.time() * 1000))
+        uuid = [0] * 36
+        uuid[8] = "_"
+        uuid[13] = "_"
+        uuid[18] = "_"
+        uuid[23] = "_"
+        uuid[14] = "4"
+
+        for i in range(36):
+            if uuid[i] != 0:
+                continue
+            r = int(random.random() * chars_len)
+            uuid[i] = chars[int((3 & r) | 8 if i == 19 else r)]
+
+        return f'verify_{scenario_title.lower()}_{"".join(uuid)}'
+
+ +
+ + + +
+
+
#   + + + def + sign_url(self, url, calc_tt_params=False, **kwargs): +
+ +
+ View Source +
    def sign_url(self, url, calc_tt_params=False, **kwargs):
+        def process(route):
+            route.abort()
+
+        tt_params = None
+        context = self._create_context()
+        page = context.new_page()
+
+        if calc_tt_params:
+            page.route(re.compile(r"(\.png)|(\.jpeg)|(\.mp4)|(x-expire)"), process)
+            page.goto(
+                kwargs.get("default_url", "https://www.tiktok.com/@redbull"),
+                wait_until="load",
+            )
+
+        verifyFp = "".join(
+            random.choice(
+                string.ascii_lowercase + string.ascii_uppercase + string.digits
+            )
+            for i in range(16)
+        )
+        if kwargs.get("gen_new_verifyFp", False):
+            verifyFp = self.gen_verifyFp()
+        else:
+            verifyFp = kwargs.get(
+                "custom_verify_fp",
+                "verify_khgp4f49_V12d4mRX_MdCO_4Wzt_Ar0k_z4RCQC9pUDpX",
+            )
+
+        if kwargs.get("custom_device_id") is not None:
+            device_id = kwargs.get("custom_device_id", None)
+        elif self.device_id is None:
+            device_id = str(random.randint(10000, 999999999))
+        else:
+            device_id = self.device_id
+
+        url = "{}&verifyFp={}&device_id={}".format(url, verifyFp, device_id)
+
+        page.add_script_tag(content=_get_acrawler())
+        evaluatedPage = page.evaluate(
+            '''() => {
+            var url = "'''
+            + url
+            + """"
+            var token = window.byted_acrawler.sign({url: url});
+            
+            return token;
+            }"""
+        )
+
+        url = "{}&_signature={}".format(url, evaluatedPage)
+
+        if calc_tt_params:
+            page.add_script_tag(content=_get_tt_params_script())
+
+            tt_params = page.evaluate(
+                """() => {
+                    return window.genXTTParams("""
+                + json.dumps(dict(parse_qsl(splitquery(url)[1])))
+                + """);
+            
+                }"""
+            )
+
+        context.close()
+        return (verifyFp, device_id, evaluatedPage, tt_params)
+
+ +
+ + + +
+
+
#   + + + def + find_redirect(self, url): +
+ +
+ View Source +
    def find_redirect(self, url):
+        self.page.goto(url, {"waitUntil": "load"})
+        self.redirect_url = self.page.url
+
+ +
+ + + +
+
+
+ + \ No newline at end of file diff --git a/docs/TikTokApi/browser_utilities/browser_interface.html b/docs/TikTokApi/browser_utilities/browser_interface.html new file mode 100644 index 00000000..ded68a65 --- /dev/null +++ b/docs/TikTokApi/browser_utilities/browser_interface.html @@ -0,0 +1,348 @@ + + + + + + + TikTokApi.browser_utilities.browser_interface API documentation + + + + + + + + +
+
+

+TikTokApi.browser_utilities.browser_interface

+ + +
+ View Source +
import abc
+
+
+class BrowserInterface(abc.ABC):
+    @abc.abstractmethod
+    def __init__(self, **kwargs):
+        pass
+
+    @abc.abstractmethod
+    def get_params(self, page) -> None:
+        pass
+
+    # Returns verify_fp, device_id, signature, tt_params
+    @abc.abstractmethod
+    def sign_url(self, calc_tt_params=False, **kwargs):
+        pass
+
+    @abc.abstractmethod
+    def _clean_up(self) -> None:
+        pass
+
+ +
+ +
+
+
+ #   + + + class + BrowserInterface(abc.ABC): +
+ +
+ View Source +
class BrowserInterface(abc.ABC):
+    @abc.abstractmethod
+    def __init__(self, **kwargs):
+        pass
+
+    @abc.abstractmethod
+    def get_params(self, page) -> None:
+        pass
+
+    # Returns verify_fp, device_id, signature, tt_params
+    @abc.abstractmethod
+    def sign_url(self, calc_tt_params=False, **kwargs):
+        pass
+
+    @abc.abstractmethod
+    def _clean_up(self) -> None:
+        pass
+
+ +
+ +

Helper class that provides a standard way to create an ABC using +inheritance.

+
+ + +
+
#   + +
@abc.abstractmethod
+ + def + get_params(self, page) -> None: +
+ +
+ View Source +
    @abc.abstractmethod
+    def get_params(self, page) -> None:
+        pass
+
+ +
+ + + +
+
+
#   + +
@abc.abstractmethod
+ + def + sign_url(self, calc_tt_params=False, **kwargs): +
+ +
+ View Source +
    @abc.abstractmethod
+    def sign_url(self, calc_tt_params=False, **kwargs):
+        pass
+
+ +
+ + + +
+
+
+ + \ No newline at end of file diff --git a/docs/TikTokApi/browser_utilities/get_acrawler.html b/docs/TikTokApi/browser_utilities/get_acrawler.html new file mode 100644 index 00000000..3e7ae465 --- /dev/null +++ b/docs/TikTokApi/browser_utilities/get_acrawler.html @@ -0,0 +1,242 @@ + + + + + + + TikTokApi.browser_utilities.get_acrawler API documentation + + + + + + + + +
+
+

+TikTokApi.browser_utilities.get_acrawler

+ + +
+ View Source +
def _get_tt_params_script():
+    return """var CryptoJS=CryptoJS||function(e,t){var r={},n=r.lib={},i=n.Base=function(){function e(){}return{extend:function(t){e.prototype=this;var r=new e;return t&&r.mixIn(t),r.hasOwnProperty("init")&&this.init!==r.init||(r.init=function(){r.$super.init.apply(this,arguments)}),r.init.prototype=r,r.$super=this,r},create:function(){var e=this.extend();return e.init.apply(e,arguments),e},init:function(){},mixIn:function(e){for(var t in e)e.hasOwnProperty(t)&&(this[t]=e[t]);e.hasOwnProperty("toString")&&(this.toString=e.toString)},clone:function(){return this.init.prototype.extend(this)}}}(),c=n.WordArray=i.extend({init:function(e,t){e=this.words=e||[],this.sigBytes=null!=t?t:4*e.length},toString:function(e){return(e||f).stringify(this)},concat:function(e){var t=this.words,r=e.words,n=this.sigBytes,i=e.sigBytes;if(this.clamp(),n%4)for(var c=0;c<i;c++){var o=r[c>>>2]>>>24-c%4*8&255;t[n+c>>>2]|=o<<24-(n+c)%4*8}else if(r.length>65535)for(c=0;c<i;c+=4)t[n+c>>>2]=r[c>>>2];else t.push.apply(t,r);return this.sigBytes+=i,this},clamp:function(){var t=this.words,r=this.sigBytes;t[r>>>2]&=4294967295<<32-r%4*8,t.length=e.ceil(r/4)},clone:function(){var e=i.clone.call(this);return e.words=this.words.slice(0),e},random:function(t){for(var r,n=[],i=function(t){t=t;var r=987654321,n=4294967295;return function(){var i=((r=36969*(65535&r)+(r>>16)&n)<<16)+(t=18e3*(65535&t)+(t>>16)&n)&n;return i/=4294967296,(i+=.5)*(e.random()>.5?1:-1)}},o=0;o<t;o+=4){var f=i(4294967296*(r||e.random()));r=987654071*f(),n.push(4294967296*f()|0)}return new c.init(n,t)}}),o=r.enc={},f=o.Hex={stringify:function(e){for(var t=e.words,r=e.sigBytes,n=[],i=0;i<r;i++){var c=t[i>>>2]>>>24-i%4*8&255;n.push((c>>>4).toString(16)),n.push((15&c).toString(16))}return n.join("")},parse:function(e){for(var t=e.length,r=[],n=0;n<t;n+=2)r[n>>>3]|=parseInt(e.substr(n,2),16)<<24-n%8*4;return new c.init(r,t/2)}},a=o.Latin1={stringify:function(e){for(var t=e.words,r=e.sigBytes,n=[],i=0;i<r;i++){var c=t[i>>>2]>>>24-i%4*8&255;n.push(String.fromCharCode(c))}return n.join("")},parse:function(e){for(var t=e.length,r=[],n=0;n<t;n++)r[n>>>2]|=(255&e.charCodeAt(n))<<24-n%4*8;return new c.init(r,t)}},s=o.Utf8={stringify:function(e){try{return decodeURIComponent(escape(a.stringify(e)))}catch(e){throw new Error("Malformed UTF-8 data")}},parse:function(e){return a.parse(unescape(encodeURIComponent(e)))}},u=n.BufferedBlockAlgorithm=i.extend({reset:function(){this._data=new c.init,this._nDataBytes=0},_append:function(e){"string"==typeof e&&(e=s.parse(e)),this._data.concat(e),this._nDataBytes+=e.sigBytes},_process:function(t){var r=this._data,n=r.words,i=r.sigBytes,o=this.blockSize,f=i/(4*o),a=(f=t?e.ceil(f):e.max((0|f)-this._minBufferSize,0))*o,s=e.min(4*a,i);if(a){for(var u=0;u<a;u+=o)this._doProcessBlock(n,u);var d=n.splice(0,a);r.sigBytes-=s}return new c.init(d,s)},clone:function(){var e=i.clone.call(this);return e._data=this._data.clone(),e},_minBufferSize:0}),d=(n.Hasher=u.extend({cfg:i.extend(),init:function(e){this.cfg=this.cfg.extend(e),this.reset()},reset:function(){u.reset.call(this),this._doReset()},update:function(e){return this._append(e),this._process(),this},finalize:function(e){return e&&this._append(e),this._doFinalize()},blockSize:16,_createHelper:function(e){return function(t,r){return new e.init(r).finalize(t)}},_createHmacHelper:function(e){return function(t,r){return new d.HMAC.init(e,r).finalize(t)}}}),r.algo={});return r}(Math);!function(){var e=CryptoJS,t=e.lib.WordArray;e.enc.Base64={stringify:function(e){var t=e.words,r=e.sigBytes,n=this._map;e.clamp();for(var i=[],c=0;c<r;c+=3)for(var o=(t[c>>>2]>>>24-c%4*8&255)<<16|(t[c+1>>>2]>>>24-(c+1)%4*8&255)<<8|t[c+2>>>2]>>>24-(c+2)%4*8&255,f=0;f<4&&c+.75*f<r;f++)i.push(n.charAt(o>>>6*(3-f)&63));var a=n.charAt(64);if(a)for(;i.length%4;)i.push(a);return i.join("")},parse:function(e){var r=e.length,n=this._map,i=this._reverseMap;if(!i){i=this._reverseMap=[];for(var c=0;c<n.length;c++)i[n.charCodeAt(c)]=c}var o=n.charAt(64);if(o){var f=e.indexOf(o);-1!==f&&(r=f)}return function(e,r,n){for(var i=[],c=0,o=0;o<r;o++)if(o%4){var f=n[e.charCodeAt(o-1)]<<o%4*2,a=n[e.charCodeAt(o)]>>>6-o%4*2;i[c>>>2]|=(f|a)<<24-c%4*8,c++}return t.create(i,c)}(e,r,i)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}}(),CryptoJS.lib.Cipher||function(e){var t=CryptoJS,r=t.lib,n=r.Base,i=r.WordArray,c=r.BufferedBlockAlgorithm,o=t.enc,f=(o.Utf8,o.Base64),a=t.algo.EvpKDF,s=r.Cipher=c.extend({cfg:n.extend(),createEncryptor:function(e,t){return this.create(this._ENC_XFORM_MODE,e,t)},createDecryptor:function(e,t){return this.create(this._DEC_XFORM_MODE,e,t)},init:function(e,t,r){this.cfg=this.cfg.extend(r),this._xformMode=e,this._key=t,this.reset()},reset:function(){c.reset.call(this),this._doReset()},process:function(e){return this._append(e),this._process()},finalize:function(e){return e&&this._append(e),this._doFinalize()},keySize:4,ivSize:4,_ENC_XFORM_MODE:1,_DEC_XFORM_MODE:2,_createHelper:function(){function e(e){return"string"==typeof e?_:v}return function(t){return{encrypt:function(r,n,i){return e(n).encrypt(t,r,n,i)},decrypt:function(r,n,i){return e(n).decrypt(t,r,n,i)}}}}()}),u=(r.StreamCipher=s.extend({_doFinalize:function(){return this._process(!0)},blockSize:1}),t.mode={}),d=r.BlockCipherMode=n.extend({createEncryptor:function(e,t){return this.Encryptor.create(e,t)},createDecryptor:function(e,t){return this.Decryptor.create(e,t)},init:function(e,t){this._cipher=e,this._iv=t}}),l=u.CBC=function(){var t=d.extend();function r(t,r,n){var i=this._iv;if(i){var c=i;this._iv=e}else c=this._prevBlock;for(var o=0;o<n;o++)t[r+o]^=c[o]}return t.Encryptor=t.extend({processBlock:function(e,t){var n=this._cipher,i=n.blockSize;r.call(this,e,t,i),n.encryptBlock(e,t),this._prevBlock=e.slice(t,t+i)}}),t.Decryptor=t.extend({processBlock:function(e,t){var n=this._cipher,i=n.blockSize,c=e.slice(t,t+i);n.decryptBlock(e,t),r.call(this,e,t,i),this._prevBlock=c}}),t}(),p=(t.pad={}).Pkcs7={pad:function(e,t){for(var r=4*t,n=r-e.sigBytes%r,c=n<<24|n<<16|n<<8|n,o=[],f=0;f<n;f+=4)o.push(c);var a=i.create(o,n);e.concat(a)},unpad:function(e){var t=255&e.words[e.sigBytes-1>>>2];e.sigBytes-=t}},h=(r.BlockCipher=s.extend({cfg:s.cfg.extend({mode:l,padding:p}),reset:function(){s.reset.call(this);var e=this.cfg,t=e.iv,r=e.mode;if(this._xformMode==this._ENC_XFORM_MODE)var n=r.createEncryptor;else{n=r.createDecryptor;this._minBufferSize=1}this._mode&&this._mode.__creator==n?this._mode.init(this,t&&t.words):(this._mode=n.call(r,this,t&&t.words),this._mode.__creator=n)},_doProcessBlock:function(e,t){this._mode.processBlock(e,t)},_doFinalize:function(){var e=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){e.pad(this._data,this.blockSize);var t=this._process(!0)}else{t=this._process(!0);e.unpad(t)}return t},blockSize:4}),r.CipherParams=n.extend({init:function(e){this.mixIn(e)},toString:function(e){return(e||this.formatter).stringify(this)}})),y=(t.format={}).OpenSSL={stringify:function(e){var t=e.ciphertext,r=e.salt;if(r)var n=i.create([1398893684,1701076831]).concat(r).concat(t);else n=t;return n.toString(f)},parse:function(e){var t=f.parse(e),r=t.words;if(1398893684==r[0]&&1701076831==r[1]){var n=i.create(r.slice(2,4));r.splice(0,4),t.sigBytes-=16}return h.create({ciphertext:t,salt:n})}},v=r.SerializableCipher=n.extend({cfg:n.extend({format:y}),encrypt:function(e,t,r,n){n=this.cfg.extend(n);var i=e.createEncryptor(r,n),c=i.finalize(t),o=i.cfg;return h.create({ciphertext:c,key:r,iv:o.iv,algorithm:e,mode:o.mode,padding:o.padding,blockSize:e.blockSize,formatter:n.format})},decrypt:function(e,t,r,n){return n=this.cfg.extend(n),t=this._parse(t,n.format),e.createDecryptor(r,n).finalize(t.ciphertext)},_parse:function(e,t){return"string"==typeof e?t.parse(e,this):e}}),b=(t.kdf={}).OpenSSL={execute:function(e,t,r,n){n||(n=i.random(8));var c=a.create({keySize:t+r}).compute(e,n),o=i.create(c.words.slice(t),4*r);return c.sigBytes=4*t,h.create({key:c,iv:o,salt:n})}},_=r.PasswordBasedCipher=v.extend({cfg:v.cfg.extend({kdf:b}),encrypt:function(e,t,r,n){var i=(n=this.cfg.extend(n)).kdf.execute(r,e.keySize,e.ivSize);n.iv=i.iv;var c=v.encrypt.call(this,e,t,i.key,n);return c.mixIn(i),c},decrypt:function(e,t,r,n){n=this.cfg.extend(n),t=this._parse(t,n.format);var i=n.kdf.execute(r,e.keySize,e.ivSize,t.salt);return n.iv=i.iv,v.decrypt.call(this,e,t,i.key,n)}})}(),CryptoJS.mode.ECB=function(){var e=CryptoJS.lib.BlockCipherMode.extend();return e.Encryptor=e.extend({processBlock:function(e,t){this._cipher.encryptBlock(e,t)}}),e.Decryptor=e.extend({processBlock:function(e,t){this._cipher.decryptBlock(e,t)}}),e}(),function(){var e=CryptoJS,t=e.lib.BlockCipher,r=e.algo,n=[],i=[],c=[],o=[],f=[],a=[],s=[],u=[],d=[],l=[];!function(){for(var e=[],t=0;t<256;t++)e[t]=t<128?t<<1:t<<1^283;var r=0,p=0;for(t=0;t<256;t++){var h=p^p<<1^p<<2^p<<3^p<<4;h=h>>>8^255&h^99,n[r]=h,i[h]=r;var y=e[r],v=e[y],b=e[v],_=257*e[h]^16843008*h;c[r]=_<<24|_>>>8,o[r]=_<<16|_>>>16,f[r]=_<<8|_>>>24,a[r]=_;_=16843009*b^65537*v^257*y^16843008*r;s[h]=_<<24|_>>>8,u[h]=_<<16|_>>>16,d[h]=_<<8|_>>>24,l[h]=_,r?(r=y^e[e[e[b^y]]],p^=e[e[p]]):r=p=1}}();var p=[0,1,2,4,8,16,32,64,128,27,54],h=r.AES=t.extend({_doReset:function(){if(!this._nRounds||this._keyPriorReset!==this._key){for(var e=this._keyPriorReset=this._key,t=e.words,r=e.sigBytes/4,i=4*((this._nRounds=r+6)+1),c=this._keySchedule=[],o=0;o<i;o++)if(o<r)c[o]=t[o];else{var f=c[o-1];o%r?r>6&&o%r==4&&(f=n[f>>>24]<<24|n[f>>>16&255]<<16|n[f>>>8&255]<<8|n[255&f]):(f=n[(f=f<<8|f>>>24)>>>24]<<24|n[f>>>16&255]<<16|n[f>>>8&255]<<8|n[255&f],f^=p[o/r|0]<<24),c[o]=c[o-r]^f}for(var a=this._invKeySchedule=[],h=0;h<i;h++){o=i-h;if(h%4)f=c[o];else f=c[o-4];a[h]=h<4||o<=4?f:s[n[f>>>24]]^u[n[f>>>16&255]]^d[n[f>>>8&255]]^l[n[255&f]]}}},encryptBlock:function(e,t){this._doCryptBlock(e,t,this._keySchedule,c,o,f,a,n)},decryptBlock:function(e,t){var r=e[t+1];e[t+1]=e[t+3],e[t+3]=r,this._doCryptBlock(e,t,this._invKeySchedule,s,u,d,l,i);r=e[t+1];e[t+1]=e[t+3],e[t+3]=r},_doCryptBlock:function(e,t,r,n,i,c,o,f){for(var a=this._nRounds,s=e[t]^r[0],u=e[t+1]^r[1],d=e[t+2]^r[2],l=e[t+3]^r[3],p=4,h=1;h<a;h++){var y=n[s>>>24]^i[u>>>16&255]^c[d>>>8&255]^o[255&l]^r[p++],v=n[u>>>24]^i[d>>>16&255]^c[l>>>8&255]^o[255&s]^r[p++],b=n[d>>>24]^i[l>>>16&255]^c[s>>>8&255]^o[255&u]^r[p++],_=n[l>>>24]^i[s>>>16&255]^c[u>>>8&255]^o[255&d]^r[p++];s=y,u=v,d=b,l=_}y=(f[s>>>24]<<24|f[u>>>16&255]<<16|f[d>>>8&255]<<8|f[255&l])^r[p++],v=(f[u>>>24]<<24|f[d>>>16&255]<<16|f[l>>>8&255]<<8|f[255&s])^r[p++],b=(f[d>>>24]<<24|f[l>>>16&255]<<16|f[s>>>8&255]<<8|f[255&u])^r[p++],_=(f[l>>>24]<<24|f[s>>>16&255]<<16|f[u>>>8&255]<<8|f[255&d])^r[p++];e[t]=y,e[t+1]=v,e[t+2]=b,e[t+3]=_},keySize:8});e.AES=t._createHelper(h)}();var a,i={};i.CryptoJS=CryptoJS,window._$jsvmprt=function(e,t,r){function n(e,t,r){return(n=function(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(e){return!1}}()?Reflect.construct:function(e,t,r){var n=[null];n.push.apply(n,t);var i=new(Function.bind.apply(e,n));return r&&function(e,t){(Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}(i,r.prototype),i}).apply(null,arguments)}function i(e){return function(e){if(Array.isArray(e)){for(var t=0,r=new Array(e.length);t<e.length;t++)r[t]=e[t];return r}}(e)||function(e){if(Symbol.iterator in Object(e)||"[object Arguments]"===Object.prototype.toString.call(e))return Array.from(e)}(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance")}()}for(var c=[],o=0,f=[],a=0,s=function(e,t){var r=e[t++],n=e[t],i=parseInt(""+r+n,16);if(i>>7==0)return[1,i];if(i>>6==2){var c=parseInt(""+e[++t]+e[++t],16);return i&=63,[2,c=(i<<=8)+c]}if(i>>6==3){var o=parseInt(""+e[++t]+e[++t],16),f=parseInt(""+e[++t]+e[++t],16);return i&=63,[3,f=(i<<=16)+(o<<=8)+f]}},u=function(e,t){var r=parseInt(""+e[t]+e[t+1],16);return r>127?-256+r:r},d=function(e,t){var r=parseInt(""+e[t]+e[t+1]+e[t+2]+e[t+3],16);return r>32767?-65536+r:r},l=function(e,t){var r=parseInt(""+e[t]+e[t+1]+e[t+2]+e[t+3]+e[t+4]+e[t+5]+e[t+6]+e[t+7],16);return r>2147483647?0+r:r},p=function(e,t){return parseInt(""+e[t]+e[t+1],16)},h=function(e,t){return parseInt(""+e[t]+e[t+1]+e[t+2]+e[t+3],16)},y=y||this||window,v=(Object.keys,e.length,0),b="",_=v;_<v+16;_++){var g=""+e[_++]+e[_];g=parseInt(g,16),b+=String.fromCharCode(g)}if("HNOJ@?RC"!=b)throw new Error("error magic number "+b);v+=16,parseInt(""+e[v]+e[v+1],16),v+=8,o=0;for(var m=0;m<4;m++){var S=v+2*m,k=""+e[S++]+e[S],C=parseInt(k,16);o+=(3&C)<<2*m}v+=16,v+=8;var B=parseInt(""+e[v]+e[v+1]+e[v+2]+e[v+3]+e[v+4]+e[v+5]+e[v+6]+e[v+7],16),x=B,w=v+=8,z=h(e,v+=B);z[1],v+=4,c={p:[],q:[]};for(var E=0;E<z;E++){for(var O=s(e,v),I=v+=2*O[0],R=c.p.length,A=0;A<O[1];A++){var M=s(e,I);c.p.push(M[1]),I+=2*M[0]}v=I,c.q.push([R,c.p.length])}var D={5:1,6:1,70:1,22:1,23:1,37:1,73:1},P={72:1},q={74:1},F={11:1,12:1,24:1,26:1,27:1,31:1},j={10:1},J={2:1,29:1,30:1,20:1},H=[],$=[];function X(e,t,r){for(var n=t;n<t+r;){var i=p(e,n);H[n]=i,n+=2,P[i]?($[n]=u(e,n),n+=2):D[i]?($[n]=d(e,n),n+=4):q[i]?($[n]=l(e,n),n+=8):F[i]?($[n]=p(e,n),n+=2):(j[i]||J[i])&&($[n]=h(e,n),n+=4)}}return U(e,w,x/2,[],t,r);function N(e,t,r,s,l,v,b,_){null==v&&(v=this);var g,m,S,k=[],C=0;b&&(g=b);var B,x,w=t,z=w+2*r;if(!_)for(;w<z;){var E=parseInt(""+e[w]+e[w+1],16);w+=2;var O=3&(B=13*E%241);if(B>>=2,O>2)O=3&B,B>>=2,O<1?(O=B)<4?(g=k[C--],k[C]=k[C]-g):O<6?(g=k[C--],k[C]=k[C]===g):O<15&&(g=k[C],k[C]=k[C-1],k[C-1]=g):O<2?(O=B)<5&&(x=p(e,w),w+=2,g=l[x],k[++C]=g):O<3?(O=B)<6||(O<8?g=k[C--]:O<12&&(x=d(e,w),f[++a]=[[w+4,x-3],0,0],w+=2*x-2)):(O=B)<2?(g=k[C--],k[C]=k[C]<g):O<9&&(x=p(e,w),w+=2,k[C]=k[C][x]);else if(O>1)if(O=3&B,B>>=2,O>2)(O=B)>5?(x=p(e,w),w+=2,k[++C]=l["$"+x]):O>3&&(x=d(e,w),f[a][0]&&!f[a][2]?f[a][1]=[w+4,x-3]:f[a++]=[0,[w+4,x-3],0],w+=2*x-2);else if(O>1){if((O=B)>2)if(k[C--])w+=4;else{if((x=d(e,w))<0){_=1,X(e,t,2*r),w+=2*x-2;break}w+=2*x-2}else if(O>0){for(x=h(e,w),g="",A=c.q[x][0];A<c.q[x][1];A++)g+=String.fromCharCode(o^c.p[A]);k[++C]=g,w+=4}}else O>0?(O=B)>1?(g=k[C--],k[C]=k[C]+g):O>-1&&(k[++C]=y):(O=B)>9?(x=p(e,w),w+=2,g=k[C--],l[x]=g):O>7?(x=h(e,w),w+=4,m=C+1,k[C-=x-1]=x?k.slice(C,m):[]):O>0&&(g=k[C--],k[C]=k[C]>g);else if(O>0){if(O=3&B,B>>=2,O<1){if((O=B)>9);else if(O>5)x=p(e,w),w+=2,k[C-=x]=0===x?new k[C]:n(k[C],i(k.slice(C+1,C+x+1)));else if(O>3){x=d(e,w);try{if(f[a][2]=1,1==(g=N(e,w+4,x-3,[],l,v,null,0))[0])return g}catch(b){if(f[a]&&f[a][1]&&1==(g=N(e,f[a][1][0],f[a][1][1],[],l,v,b,0))[0])return g}finally{if(f[a]&&f[a][0]&&1==(g=N(e,f[a][0][0],f[a][0][1],[],l,v,null,0))[0])return g;f[a]=0,a--}w+=2*x-2}}else if(O<2){if((O=B)>12)k[++C]=u(e,w),w+=2;else if(O>8){for(x=h(e,w),O="",A=c.q[x][0];A<c.q[x][1];A++)O+=String.fromCharCode(o^c.p[A]);w+=4,k[C]=k[C][O]}}else if(O<3)(O=B)>11?(g=k[C],k[++C]=g):O>0&&(k[++C]=g);else if((O=B)<1)k[C]=!k[C];else if(O<3){if((x=d(e,w))<0){_=1,X(e,t,2*r),w+=2*x-2;break}w+=2*x-2}}else if(O=3&B,B>>=2,O>2)(O=B)<1&&(k[++C]=null);else if(O>1){if((O=B)<9){for(g=k[C--],x=h(e,w),O="",A=c.q[x][0];A<c.q[x][1];A++)O+=String.fromCharCode(o^c.p[A]);w+=4,k[C--][O]=g}}else if(O>0)(O=B)<4?(m=k[C--],(O=k[C]).x===N?O.y>=1?k[C]=U(e,O.c,O.l,[m],O.z,S,null,1):(k[C]=U(e,O.c,O.l,[m],O.z,S,null,0),O.y++):k[C]=O(m)):O<6&&(k[C-=1]=k[C][k[C+1]]);else{if((O=B)<1)return[1,k[C--]];O<14?(m=k[C--],S=k[C--],(O=k[C--]).x===N?O.y>=1?k[++C]=U(e,O.c,O.l,m,O.z,S,null,1):(k[++C]=U(e,O.c,O.l,m,O.z,S,null,0),O.y++):k[++C]=O.apply(S,m)):O<16&&(x=d(e,w),(I=function t(){var r=arguments;return t.y>0||t.y++,U(e,t.c,t.l,r,t.z,this,null,0)}).c=w+4,I.l=x-2,I.x=N,I.y=0,I.z=l,k[C]=I,w+=2*x-2)}}if(_)for(;w<z;)if(E=H[w],w+=2,O=3&(B=13*E%241),B>>=2,O<1)if(O=3&B,B>>=2,O>2)(O=B)<1&&(k[++C]=null);else if(O>1){if((O=B)<9){for(g=k[C--],x=$[w],O="",A=c.q[x][0];A<c.q[x][1];A++)O+=String.fromCharCode(o^c.p[A]);w+=4,k[C--][O]=g}}else if(O>0)(O=B)<4?(m=k[C--],(O=k[C]).x===N?O.y>=1?k[C]=U(e,O.c,O.l,[m],O.z,S,null,1):(k[C]=U(e,O.c,O.l,[m],O.z,S,null,0),O.y++):k[C]=O(m)):O<6&&(k[C-=1]=k[C][k[C+1]]);else{var I;if((O=B)>14)x=$[w],(I=function t(){var r=arguments;return t.y>0||t.y++,U(e,t.c,t.l,r,t.z,this,null,0)}).c=w+4,I.l=x-2,I.x=N,I.y=0,I.z=l,k[C]=I,w+=2*x-2;else if(O>12)m=k[C--],S=k[C--],(O=k[C--]).x===N?O.y>=1?k[++C]=U(e,O.c,O.l,m,O.z,S,null,1):(k[++C]=U(e,O.c,O.l,m,O.z,S,null,0),O.y++):k[++C]=O.apply(S,m);else if(O>-1)return[1,k[C--]]}else if(O<2)if(O=3&B,B>>=2,O>2)(O=B)<1?k[C]=!k[C]:O<3&&(w+=2*(x=$[w])-2);else if(O>1)(O=B)<2?k[++C]=g:O<13&&(g=k[C],k[++C]=g);else if(O>0)if((O=B)<10){for(x=$[w],O="",A=c.q[x][0];A<c.q[x][1];A++)O+=String.fromCharCode(o^c.p[A]);w+=4,k[C]=k[C][O]}else O<14&&(k[++C]=$[w],w+=2);else if((O=B)<5){x=$[w];try{if(f[a][2]=1,1==(g=N(e,w+4,x-3,[],l,v,null,0))[0])return g}catch(b){if(f[a]&&f[a][1]&&1==(g=N(e,f[a][1][0],f[a][1][1],[],l,v,b,0))[0])return g}finally{if(f[a]&&f[a][0]&&1==(g=N(e,f[a][0][0],f[a][0][1],[],l,v,null,0))[0])return g;f[a]=0,a--}w+=2*x-2}else O<7&&(x=$[w],w+=2,k[C-=x]=0===x?new k[C]:n(k[C],i(k.slice(C+1,C+x+1))));else if(O<3)if(O=3&B,B>>=2,O<1)(O=B)>9?(x=$[w],w+=2,g=k[C--],l[x]=g):O>7?(x=$[w],w+=4,m=C+1,k[C-=x-1]=x?k.slice(C,m):[]):O>0&&(g=k[C--],k[C]=k[C]>g);else if(O<2)(O=B)>1?(g=k[C--],k[C]=k[C]+g):O>-1&&(k[++C]=y);else if(O<3)if((O=B)<2){for(x=$[w],g="",A=c.q[x][0];A<c.q[x][1];A++)g+=String.fromCharCode(o^c.p[A]);k[++C]=g,w+=4}else O<4&&(k[C--]?w+=4:w+=2*(x=$[w])-2);else(O=B)>5?(x=$[w],w+=2,k[++C]=l["$"+x]):O>3&&(x=$[w],f[a][0]&&!f[a][2]?f[a][1]=[w+4,x-3]:f[a++]=[0,[w+4,x-3],0],w+=2*x-2);else O=3&B,B>>=2,O<1?(O=B)<4?(g=k[C--],k[C]=k[C]-g):O<6?(g=k[C--],k[C]=k[C]===g):O<15&&(g=k[C],k[C]=k[C-1],k[C-1]=g):O<2?(O=B)<5&&(x=$[w],w+=2,g=l[x],k[++C]=g):O<3?(O=B)>10?(x=$[w],f[++a]=[[w+4,x-3],0,0],w+=2*x-2):O>6&&(g=k[C--]):(O=B)<2?(g=k[C--],k[C]=k[C]<g):O<9&&(x=$[w],w+=2,k[C]=k[C][x]);return[0,null]}function U(e,t,r,n,i,c,o,f){var a,s;null==c&&(c=this),i&&!i.d&&(i.d=0,i.$0=i,i[1]={});var u={},d=u.d=i?i.d+1:0;for(u["$"+d]=u,s=0;s<d;s++)u[a="$"+s]=i[a];for(s=0,d=u.length=n.length;s<d;s++)u[s]=n[s];return f&&!H[t]&&X(e,t,2*r),H[t]?N(e,t,r,0,u,c,null,1)[1]:N(e,t,r,0,u,c,null,0)[1]}},a=[i,,"undefined"!=typeof sessionStorage?sessionStorage:void 0,"undefined"!=typeof console?console:void 0,"undefined"!=typeof document?document:void 0,"undefined"!=typeof navigator?navigator:void 0,"undefined"!=typeof screen?screen:void 0,"undefined"!=typeof Intl?Intl:void 0,"undefined"!=typeof Array?Array:void 0,"undefined"!=typeof Object?Object:void 0],window._$jsvmprt("484e4f4a403f524300332d0511788d78e08713dc000000000000080a1b000b001e00011f0002000025003d46000306001a271f0c1b000b03221e0002240200030a0001101c18010005001c1b000b02221e00042418000a00011022011700061c18010007001f010200051f020200061f030200071f040200002500121b010b011b010b03041b010b043e001f050200002501981b000b041e0008221e000924131e000a02000b0200001a020a0001101f061800220117000a1c131e000c1a001f07460003060006271f2c050157131e000c1a002202000d1d000e2202000f1d00102218041d00112218071e00121d00132218071e00141d00152218071e0016220117000a1c131e000c1a001e001522011700071c0200001d00172218071e00181d0019221b000b041e001a1d001b221b010b011b010b02041d001c221b000b051e001d1d001e221b000b061e001f1d0020221b000b061e00211d0022221b000b051e00231d0024221b000b051e00251d0026221b000b051e00271d0028221b000b051e00291d002a221b000b051e002b1d002c22180622011700071c0200004801191d002d2218071e002e1d002f221b000b07221e0030240a000010221e0031240a0000101e00321d00332218011d00342218021d00352213221e0036240200370a0001101e00381d003922131e003a1e003b1d003c1f081b010b05260a00001017000b180802003d1d003e1b000b051e003f17000a180818031d004018080007131e000c1a00001f0602000025007f131e000c1a00221b000b051e001d1d001e221b000b061e001f1d0020221b000b061e00211d0022221b000b051e00231d0024221b000b051e00251d0026221b000b051e00271d0028221b000b051e00291d002a221b000b051e002b1d002c221b000b07221e0030240a000010221e0031240a0000101e00321d0033001f070200002501520200411f060a00001f0702000025005d1800221e0042240a0000101f0618061e003b1f07180718013a1700251b000b0818011807294801281a01221e0043240200440a0001101806281f0616001c18071801391700141806221e004524480018010a0002101f061806001f080200002500731b020b0826180148100a0002101f061b010b001e00461e0047221e00482418060a0001101f071b010b001e0049221e004a2418001807131e000c1a002218071d004b221b010b001e004c1e004d1d004c221b010b001e004e1e004f1d00500a0003101f081808221e0042240a000010001f091b000b09221e00512418000a000110221e0052240200002500241800020053281b020b00180019281f061b020b07221e00542418060a0001101c000a0001101c1807221e0054240200550a0001101c1809261807221e0043240200560a00011018060a0002101f0a180a001f081b000b0118061d00571b000b0118071d00581b000b0118081d005900005a000852636861657e5b42046670637f1962746262787e7f42657e6370767431767465317770787d7475077674655865747c166674737061613c62746262787e7f3c637477746374630c747f6574634e7c7465797e750970767447746378776806727e7e7a7874057c706572790643747654696110624e674e6674734e78752c394d663a38065e737b7472650420282929037078750a65787a657e7a4e667473087061614e7f707c740f7574677872744e617d7065777e637c0435667875097574677872744e78750735637476787e7f06637476787e7f0535646274630f6163787e637865684e637476787e7f03357e62027e6208637477746363746307637477746374630c637e7e654e637477746374630d727e7e7a7874547f70737d74750e727e7e7a78744e747f70737d74750566787565790c62726374747f4e6678756579067974787679650d62726374747f4e797478767965087d707f76647076741073637e666274634e7d707f766470767408617d7065777e637c1073637e666274634e617d7065777e637c0b706161527e75745f707c740c73637e666274634e7f707c740a70616147746362787e7f0f73637e666274634e67746362787e7f067e7f5d787f740e73637e666274634e7e7f7d787f7408677463787768576109357d707f76647076740c7061614e7d707f76647076740e5570657445787c74577e637c70650f6374627e7d6774755e6165787e7f620865787c744b7e7f740d65787c746b7e7f744e7f707c740f78624e617076744e67786278737d740b777e7264624e62657065740a7c706572795c747578701a39757862617d70683c7c7e75742b3177647d7d62726374747f38077c7065727974620d78624e77647d7d62726374747f07797862657e6368067d747f7665790b797862657e63684e7d747f04202524281962747264637865684e677463787778727065787e7f4e7078750a767465537065657463680c737065657463684e787f777e12667473706161203f213a232123202127232908657e426563787f76047b7e787f012105627d78727403747f7204446577290561706362740350544207747f7263686165027867047c7e7574035253520361707505417a7262260761707575787f76047a74686207777e6354707279012c04616462790f78624e747f7263686165787e7f2c2001370f767465527e7c7c7e7f417063707c6211767465547062684378627a417063707c620d747f7263686165417063707c62",a);var c=a[1],s=c.getCommonParams,u=c.getEasyRiskParams,l=c.encryptParams;window.genXTTParams=function(e){return l(e)};"""
+
+
+def _get_acrawler():
+    return """Function(function(t){return'w={S(S,K){if(!a[S]){a[S]={};for(y=0;y<S;y)a[S][S.charAt(y)]=y}a[S][K]}K=String.fromCharCode,a={},y={x:(K){null==K?"":""==K?null:y.y(K,32,(a){S("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",K.charAt(a})},y:(S,a,y){p,m,o,T,l,r,k,i=[],J=4,q=4,j=3,I="",b=[],z={val:y(0),position:a,index:1};for(p=0;p<3;p+=1)i[p]=p;for(o=0,l=Math.pow(2,2),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;switch(o){case 0:for(o=0,l=Math.pow(2,8),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;k=K(o)1:for(o=0,l=Math.pow(2,16),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;k=K(o)2:""}for(i[3]=k,m=k,b.push(k);;){if(z.index>S)"";for(o=0,l=Math.pow(2,j),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;switch(k=o){case 0:for(o=0,l=Math.pow(2,8),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;i[q]=K(o),k=q-1,J--1:for(o=0,l=Math.pow(2,16),r=1;r!=l;)T=z.val&z.position,z.position>>=1,0==z.position&&(z.position=a,z.val=y(z.index,o|=(T>0?1:0)*r,r<<=1;i[q]=K(o),k=q-1,J--2:b.join("")}if(0==J&&(J=Math.pow(2,j),j),i[k])I=i[k]if(k!==q)null;I=m+m.charAt(0)}b.push(I),i[q]=m+I.charAt(0),m=I,0==--J&&(J=Math.pow(2,j),j)}}};y};""==typeof define&&define.amd?define({w}):"undefined"!=typeof module&&null!=module?module.exports=w:"undefined"!=typeof angular&&null!=angular&&angular.module("w",[]).factory("w",{w}),eval(w.x("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";'.replace(/[-]/g,function(m){return t[m.charCodeAt(0)&15]})}("var function ().length++return ));break;case ;else{".split("")))();"""
+
+ +
+ +
+
+ + \ No newline at end of file diff --git a/docs/TikTokApi/browser_utilities/stealth.html b/docs/TikTokApi/browser_utilities/stealth.html new file mode 100644 index 00000000..c233ca87 --- /dev/null +++ b/docs/TikTokApi/browser_utilities/stealth.html @@ -0,0 +1,1455 @@ + + + + + + + TikTokApi.browser_utilities.stealth API documentation + + + + + + + + +
+
+

+TikTokApi.browser_utilities.stealth

+ + +
+ View Source +
import re
+
+
+def chrome_runtime(page) -> None:
+    page.evaluateOnNewDocument(
+        """
+() => {
+    window.chrome = {
+        runtime: {}
+    }
+}
+"""
+    )
+
+
+def console_debug(page) -> None:
+    page.evaluateOnNewDocument(
+        """
+() => {
+    window.console.debug = () => {
+        return null
+    }
+}
+"""
+    )
+
+
+def iframe_content_window(page) -> None:
+    page.evaluateOnNewDocument(
+        """
+() => {
+  try {
+    // Adds a contentWindow proxy to the provided iframe element
+    const addContentWindowProxy = iframe => {
+      const contentWindowProxy = {
+        get(target, key) {
+          // Now to the interesting part:
+          // We actually make this thing behave like a regular iframe window,
+          // by intercepting calls to e.g. `.self` and redirect it to the correct thing. :)
+          // That makes it possible for these assertions to be correct:
+          // iframe.contentWindow.self === window.top // must be false
+          if (key === 'self') {
+            return this
+          }
+          // iframe.contentWindow.frameElement === iframe // must be true
+          if (key === 'frameElement') {
+            return iframe
+          }
+          return Reflect.get(target, key)
+        }
+      }
+      if (!iframe.contentWindow) {
+        const proxy = new Proxy(window, contentWindowProxy)
+        Object.defineProperty(iframe, 'contentWindow', {
+          get() {
+            return proxy
+          },
+          set(newValue) {
+            return newValue // contentWindow is immutable
+          },
+          enumerable: true,
+          configurable: false
+        })
+      }
+    }
+    // Handles iframe element creation, augments `srcdoc` property so we can intercept further
+    const handleIframeCreation = (target, thisArg, args) => {
+      const iframe = target.apply(thisArg, args)
+      // We need to keep the originals around
+      const _iframe = iframe
+      const _srcdoc = _iframe.srcdoc
+      // Add hook for the srcdoc property
+      // We need to be very surgical here to not break other iframes by accident
+      Object.defineProperty(iframe, 'srcdoc', {
+        configurable: true, // Important, so we can reset this later
+        get: function() {
+          return _iframe.srcdoc
+        },
+        set: function(newValue) {
+          addContentWindowProxy(this)
+          // Reset property, the hook is only needed once
+          Object.defineProperty(iframe, 'srcdoc', {
+            configurable: false,
+            writable: false,
+            value: _srcdoc
+          })
+          _iframe.srcdoc = newValue
+        }
+      })
+      return iframe
+    }
+    // Adds a hook to intercept iframe creation events
+    const addIframeCreationSniffer = () => {
+      /* global document */
+      const createElement = {
+        // Make toString() native
+        get(target, key) {
+          return Reflect.get(target, key)
+        },
+        apply: function(target, thisArg, args) {
+          const isIframe =
+            args && args.length && `${args[0]}`.toLowerCase() === 'iframe'
+          if (!isIframe) {
+            // Everything as usual
+            return target.apply(thisArg, args)
+          } else {
+            return handleIframeCreation(target, thisArg, args)
+          }
+        }
+      }
+      // All this just due to iframes with srcdoc bug
+      document.createElement = new Proxy(
+        document.createElement,
+        createElement
+      )
+    }
+    // Let's go
+    addIframeCreationSniffer()
+  } catch (err) {
+    // console.warn(err)
+  }
+}
+"""
+    )
+
+
+def media_codecs(page) -> None:
+    page.evaluateOnNewDocument(
+        """
+    () => {
+  try {
+    /**
+     * Input might look funky, we need to normalize it so e.g. whitespace isn't an issue for our spoofing.
+     *
+     * @example
+     * video/webm; codecs="vp8, vorbis"
+     * video/mp4; codecs="avc1.42E01E"
+     * audio/x-m4a;
+     * audio/ogg; codecs="vorbis"
+     * @param {String} arg
+     */
+    const parseInput = arg => {
+      const [mime, codecStr] = arg.trim().split(';')
+      let codecs = []
+      if (codecStr && codecStr.includes('codecs="')) {
+        codecs = codecStr
+          .trim()
+          .replace(`codecs="`, '')
+          .replace(`"`, '')
+          .trim()
+          .split(',')
+          .filter(x => !!x)
+          .map(x => x.trim())
+      }
+      return { mime, codecStr, codecs }
+    }
+    /* global HTMLMediaElement */
+    const canPlayType = {
+      // Make toString() native
+      get(target, key) {
+        // Mitigate Chromium bug (#130)
+        if (typeof target[key] === 'function') {
+          return target[key].bind(target)
+        }
+        return Reflect.get(target, key)
+      },
+      // Intercept certain requests
+      apply: function(target, ctx, args) {
+        if (!args || !args.length) {
+          return target.apply(ctx, args)
+        }
+        const { mime, codecs } = parseInput(args[0])
+        // This specific mp4 codec is missing in Chromium
+        if (mime === 'video/mp4') {
+          if (codecs.includes('avc1.42E01E')) {
+            return 'probably'
+          }
+        }
+        // This mimetype is only supported if no codecs are specified
+        if (mime === 'audio/x-m4a' && !codecs.length) {
+          return 'maybe'
+        }
+        // This mimetype is only supported if no codecs are specified
+        if (mime === 'audio/aac' && !codecs.length) {
+          return 'probably'
+        }
+        // Everything else as usual
+        return target.apply(ctx, args)
+      }
+    }
+    HTMLMediaElement.prototype.canPlayType = new Proxy(
+      HTMLMediaElement.prototype.canPlayType,
+      canPlayType
+    )
+  } catch (err) {}
+}
+"""
+    )
+
+
+def navigator_languages(page) -> None:
+    page.evaluateOnNewDocument(
+        """
+() => {
+    Object.defineProperty(navigator, 'languages', {
+        get: () => ['en-US', 'en']
+    })
+}
+    """
+    )
+
+
+def navigator_permissions(page) -> None:
+    page.evaluateOnNewDocument(
+        """
+() => {
+    const originalQuery = window.navigator.permissions.query
+    window.navigator.permissions.__proto__.query = parameters =>
+        parameters.name === 'notifications'
+            ? Promise.resolve({ state: Notification.permission })
+            : originalQuery(parameters)
+    const oldCall = Function.prototype.call
+    function call () {
+        return oldCall.apply(this, arguments)
+    }
+    Function.prototype.call = call
+    const nativeToStringFunctionString = Error.toString().replace(
+        /Error/g,
+        'toString'
+    )
+    const oldToString = Function.prototype.toString
+    function functionToString () {
+        if (this === window.navigator.permissions.query) {
+            return 'function query() { [native code] }'
+        }
+        if (this === functionToString) {
+            return nativeToStringFunctionString
+        }
+        return oldCall.call(oldToString, this)
+    }
+    Function.prototype.toString = functionToString
+}
+    """
+    )
+
+
+def navigator_plugins(page) -> None:
+    page.evaluateOnNewDocument(
+        """
+() => {
+    function mockPluginsAndMimeTypes() {
+        const makeFnsNative = (fns = []) => {
+            const oldCall = Function.prototype.call
+            function call() {
+                return oldCall.apply(this, arguments)
+            }
+            Function.prototype.call = call
+            const nativeToStringFunctionString = Error.toString().replace(
+                /Error/g,
+                'toString'
+            )
+            const oldToString = Function.prototype.toString
+            function functionToString() {
+                for (const fn of fns) {
+                    if (this === fn.ref) {
+                        return `function ${fn.name}() { [native code] }`
+                    }
+                }
+                if (this === functionToString) {
+                    return nativeToStringFunctionString
+                }
+                return oldCall.call(oldToString, this)
+            }
+            Function.prototype.toString = functionToString
+        }
+        const mockedFns = []
+        const fakeData = {
+            mimeTypes: [
+                {
+                    type: 'application/pdf',
+                    suffixes: 'pdf',
+                    description: '',
+                    __pluginName: 'Chrome PDF Viewer'
+                },
+                {
+                    type: 'application/x-google-chrome-pdf',
+                    suffixes: 'pdf',
+                    description: 'Portable Document Format',
+                    __pluginName: 'Chrome PDF Plugin'
+                },
+                {
+                    type: 'application/x-nacl',
+                    suffixes: '',
+                    description: 'Native Client Executable',
+                    enabledPlugin: Plugin,
+                    __pluginName: 'Native Client'
+                },
+                {
+                    type: 'application/x-pnacl',
+                    suffixes: '',
+                    description: 'Portable Native Client Executable',
+                    __pluginName: 'Native Client'
+                }
+            ],
+            plugins: [
+                {
+                    name: 'Chrome PDF Plugin',
+                    filename: 'internal-pdf-viewer',
+                    description: 'Portable Document Format'
+                },
+                {
+                    name: 'Chrome PDF Viewer',
+                    filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai',
+                    description: ''
+                },
+                {
+                    name: 'Native Client',
+                    filename: 'internal-nacl-plugin',
+                    description: ''
+                }
+            ],
+            fns: {
+                namedItem: instanceName => {
+                    const fn = function (name) {
+                        if (!arguments.length) {
+                            throw new TypeError(
+                                `Failed to execute 'namedItem' on '${instanceName}': 1 argument required, but only 0 present.`
+                            )
+                        }
+                        return this[name] || null
+                    }
+                    mockedFns.push({ ref: fn, name: 'namedItem' })
+                    return fn
+                },
+                item: instanceName => {
+                    const fn = function (index) {
+                        if (!arguments.length) {
+                            throw new TypeError(
+                                `Failed to execute 'namedItem' on '${instanceName}': 1 argument required, but only 0 present.`
+                            )
+                        }
+                        return this[index] || null
+                    }
+                    mockedFns.push({ ref: fn, name: 'item' })
+                    return fn
+                },
+                refresh: instanceName => {
+                    const fn = function () {
+                        return undefined
+                    }
+                    mockedFns.push({ ref: fn, name: 'refresh' })
+                    return fn
+                }
+            }
+        }
+        const getSubset = (keys, obj) =>
+            keys.reduce((a, c) => ({ ...a, [c]: obj[c] }), {})
+        function generateMimeTypeArray() {
+            const arr = fakeData.mimeTypes
+                .map(obj => getSubset(['type', 'suffixes', 'description'], obj))
+                .map(obj => Object.setPrototypeOf(obj, MimeType.prototype))
+            arr.forEach(obj => {
+                arr[obj.type] = obj
+            })
+            arr.namedItem = fakeData.fns.namedItem('MimeTypeArray')
+            arr.item = fakeData.fns.item('MimeTypeArray')
+            return Object.setPrototypeOf(arr, MimeTypeArray.prototype)
+        }
+        const mimeTypeArray = generateMimeTypeArray()
+        Object.defineProperty(navigator, 'mimeTypes', {
+            get: () => mimeTypeArray
+        })
+        function generatePluginArray() {
+            const arr = fakeData.plugins
+                .map(obj => getSubset(['name', 'filename', 'description'], obj))
+                .map(obj => {
+                    const mimes = fakeData.mimeTypes.filter(
+                        m => m.__pluginName === obj.name
+                    )
+                    mimes.forEach((mime, index) => {
+                        navigator.mimeTypes[mime.type].enabledPlugin = obj
+                        obj[mime.type] = navigator.mimeTypes[mime.type]
+                        obj[index] = navigator.mimeTypes[mime.type]
+                    })
+                    obj.length = mimes.length
+                    return obj
+                })
+                .map(obj => {
+                    obj.namedItem = fakeData.fns.namedItem('Plugin')
+                    obj.item = fakeData.fns.item('Plugin')
+                    return obj
+                })
+                .map(obj => Object.setPrototypeOf(obj, Plugin.prototype))
+            arr.forEach(obj => {
+                arr[obj.name] = obj
+            })
+            arr.namedItem = fakeData.fns.namedItem('PluginArray')
+            arr.item = fakeData.fns.item('PluginArray')
+            arr.refresh = fakeData.fns.refresh('PluginArray')
+            return Object.setPrototypeOf(arr, PluginArray.prototype)
+        }
+        const pluginArray = generatePluginArray()
+        Object.defineProperty(navigator, 'plugins', {
+            get: () => pluginArray
+        })
+        makeFnsNative(mockedFns)
+    }
+    try {
+        const isPluginArray = navigator.plugins instanceof PluginArray
+        const hasPlugins = isPluginArray && navigator.plugins.length > 0
+        if (isPluginArray && hasPlugins) {
+            return
+        }
+        mockPluginsAndMimeTypes()
+    } catch (err) { }
+}
+"""
+    )
+
+
+def navigator_webdriver(page) -> None:
+    page.evaluateOnNewDocument(
+        """
+() => {
+    Object.defineProperty(window, 'navigator', {
+    value: new Proxy(navigator, {
+      has: (target, key) => (key === 'webdriver' ? false : key in target),
+      get: (target, key) =>
+        key === 'webdriver'
+          ? undefined
+          : typeof target[key] === 'function'
+          ? target[key].bind(target)
+          : target[key]
+    })
+  })
+}
+    """
+    )
+
+
+def user_agent(page) -> None:
+    return
+    ua = page.browser.userAgent()
+    ua = ua.replace("HeadlessChrome", "Chrome")  # hide headless nature
+    ua = re.sub(
+        r"\(([^)]+)\)", "(Windows NT 10.0; Win64; x64)", ua, 1
+    )  # ensure windows
+
+    page.setUserAgent(ua)
+
+
+def webgl_vendor(page) -> None:
+    page.evaluateOnNewDocument(
+        """
+() => {
+    try {
+        const getParameter = WebGLRenderingContext.prototype.getParameter
+        WebGLRenderingContext.prototype.getParameter = function (parameter) {
+          if (parameter === 37445) {
+            return 'Intel Inc.'
+          }
+          if (parameter === 37446) {
+            return 'Intel Iris OpenGL Engine'
+          }
+          return getParameter.apply(this, [parameter])
+        }
+      } catch (err) {}
+}
+"""
+    )
+
+
+def window_outerdimensions(page) -> None:
+    page.evaluateOnNewDocument(
+        """
+() => {
+    try {
+        if (window.outerWidth && window.outerHeight) {
+            return
+        }
+        const windowFrame = 85
+        window.outerWidth = window.innerWidth
+        window.outerHeight = window.innerHeight + windowFrame
+    } catch (err) { }
+}
+"""
+    )
+
+
+def stealth(page) -> None:
+    # chrome_runtime(page)
+    console_debug(page)
+    iframe_content_window(page)
+    # navigator_languages(page)
+    navigator_permissions(page)
+    navigator_plugins(page)
+    navigator_webdriver(page)
+    # navigator_vendor(page)
+    user_agent(page)
+    webgl_vendor(page)
+    window_outerdimensions(page)
+    media_codecs(page)
+
+ +
+ +
+
+
#   + + + def + chrome_runtime(page) -> None: +
+ +
+ View Source +
def chrome_runtime(page) -> None:
+    page.evaluateOnNewDocument(
+        """
+() => {
+    window.chrome = {
+        runtime: {}
+    }
+}
+"""
+    )
+
+ +
+ + + +
+
+
#   + + + def + console_debug(page) -> None: +
+ +
+ View Source +
def console_debug(page) -> None:
+    page.evaluateOnNewDocument(
+        """
+() => {
+    window.console.debug = () => {
+        return null
+    }
+}
+"""
+    )
+
+ +
+ + + +
+
+
#   + + + def + iframe_content_window(page) -> None: +
+ +
+ View Source +
def iframe_content_window(page) -> None:
+    page.evaluateOnNewDocument(
+        """
+() => {
+  try {
+    // Adds a contentWindow proxy to the provided iframe element
+    const addContentWindowProxy = iframe => {
+      const contentWindowProxy = {
+        get(target, key) {
+          // Now to the interesting part:
+          // We actually make this thing behave like a regular iframe window,
+          // by intercepting calls to e.g. `.self` and redirect it to the correct thing. :)
+          // That makes it possible for these assertions to be correct:
+          // iframe.contentWindow.self === window.top // must be false
+          if (key === 'self') {
+            return this
+          }
+          // iframe.contentWindow.frameElement === iframe // must be true
+          if (key === 'frameElement') {
+            return iframe
+          }
+          return Reflect.get(target, key)
+        }
+      }
+      if (!iframe.contentWindow) {
+        const proxy = new Proxy(window, contentWindowProxy)
+        Object.defineProperty(iframe, 'contentWindow', {
+          get() {
+            return proxy
+          },
+          set(newValue) {
+            return newValue // contentWindow is immutable
+          },
+          enumerable: true,
+          configurable: false
+        })
+      }
+    }
+    // Handles iframe element creation, augments `srcdoc` property so we can intercept further
+    const handleIframeCreation = (target, thisArg, args) => {
+      const iframe = target.apply(thisArg, args)
+      // We need to keep the originals around
+      const _iframe = iframe
+      const _srcdoc = _iframe.srcdoc
+      // Add hook for the srcdoc property
+      // We need to be very surgical here to not break other iframes by accident
+      Object.defineProperty(iframe, 'srcdoc', {
+        configurable: true, // Important, so we can reset this later
+        get: function() {
+          return _iframe.srcdoc
+        },
+        set: function(newValue) {
+          addContentWindowProxy(this)
+          // Reset property, the hook is only needed once
+          Object.defineProperty(iframe, 'srcdoc', {
+            configurable: false,
+            writable: false,
+            value: _srcdoc
+          })
+          _iframe.srcdoc = newValue
+        }
+      })
+      return iframe
+    }
+    // Adds a hook to intercept iframe creation events
+    const addIframeCreationSniffer = () => {
+      /* global document */
+      const createElement = {
+        // Make toString() native
+        get(target, key) {
+          return Reflect.get(target, key)
+        },
+        apply: function(target, thisArg, args) {
+          const isIframe =
+            args && args.length && `${args[0]}`.toLowerCase() === 'iframe'
+          if (!isIframe) {
+            // Everything as usual
+            return target.apply(thisArg, args)
+          } else {
+            return handleIframeCreation(target, thisArg, args)
+          }
+        }
+      }
+      // All this just due to iframes with srcdoc bug
+      document.createElement = new Proxy(
+        document.createElement,
+        createElement
+      )
+    }
+    // Let's go
+    addIframeCreationSniffer()
+  } catch (err) {
+    // console.warn(err)
+  }
+}
+"""
+    )
+
+ +
+ + + +
+
+
#   + + + def + media_codecs(page) -> None: +
+ +
+ View Source +
def media_codecs(page) -> None:
+    page.evaluateOnNewDocument(
+        """
+    () => {
+  try {
+    /**
+     * Input might look funky, we need to normalize it so e.g. whitespace isn't an issue for our spoofing.
+     *
+     * @example
+     * video/webm; codecs="vp8, vorbis"
+     * video/mp4; codecs="avc1.42E01E"
+     * audio/x-m4a;
+     * audio/ogg; codecs="vorbis"
+     * @param {String} arg
+     */
+    const parseInput = arg => {
+      const [mime, codecStr] = arg.trim().split(';')
+      let codecs = []
+      if (codecStr && codecStr.includes('codecs="')) {
+        codecs = codecStr
+          .trim()
+          .replace(`codecs="`, '')
+          .replace(`"`, '')
+          .trim()
+          .split(',')
+          .filter(x => !!x)
+          .map(x => x.trim())
+      }
+      return { mime, codecStr, codecs }
+    }
+    /* global HTMLMediaElement */
+    const canPlayType = {
+      // Make toString() native
+      get(target, key) {
+        // Mitigate Chromium bug (#130)
+        if (typeof target[key] === 'function') {
+          return target[key].bind(target)
+        }
+        return Reflect.get(target, key)
+      },
+      // Intercept certain requests
+      apply: function(target, ctx, args) {
+        if (!args || !args.length) {
+          return target.apply(ctx, args)
+        }
+        const { mime, codecs } = parseInput(args[0])
+        // This specific mp4 codec is missing in Chromium
+        if (mime === 'video/mp4') {
+          if (codecs.includes('avc1.42E01E')) {
+            return 'probably'
+          }
+        }
+        // This mimetype is only supported if no codecs are specified
+        if (mime === 'audio/x-m4a' && !codecs.length) {
+          return 'maybe'
+        }
+        // This mimetype is only supported if no codecs are specified
+        if (mime === 'audio/aac' && !codecs.length) {
+          return 'probably'
+        }
+        // Everything else as usual
+        return target.apply(ctx, args)
+      }
+    }
+    HTMLMediaElement.prototype.canPlayType = new Proxy(
+      HTMLMediaElement.prototype.canPlayType,
+      canPlayType
+    )
+  } catch (err) {}
+}
+"""
+    )
+
+ +
+ + + +
+ + + + +
+
#   + + + def + user_agent(page) -> None: +
+ +
+ View Source +
def user_agent(page) -> None:
+    return
+    ua = page.browser.userAgent()
+    ua = ua.replace("HeadlessChrome", "Chrome")  # hide headless nature
+    ua = re.sub(
+        r"\(([^)]+)\)", "(Windows NT 10.0; Win64; x64)", ua, 1
+    )  # ensure windows
+
+    page.setUserAgent(ua)
+
+ +
+ + + +
+
+
#   + + + def + webgl_vendor(page) -> None: +
+ +
+ View Source +
def webgl_vendor(page) -> None:
+    page.evaluateOnNewDocument(
+        """
+() => {
+    try {
+        const getParameter = WebGLRenderingContext.prototype.getParameter
+        WebGLRenderingContext.prototype.getParameter = function (parameter) {
+          if (parameter === 37445) {
+            return 'Intel Inc.'
+          }
+          if (parameter === 37446) {
+            return 'Intel Iris OpenGL Engine'
+          }
+          return getParameter.apply(this, [parameter])
+        }
+      } catch (err) {}
+}
+"""
+    )
+
+ +
+ + + +
+
+
#   + + + def + window_outerdimensions(page) -> None: +
+ +
+ View Source +
def window_outerdimensions(page) -> None:
+    page.evaluateOnNewDocument(
+        """
+() => {
+    try {
+        if (window.outerWidth && window.outerHeight) {
+            return
+        }
+        const windowFrame = 85
+        window.outerWidth = window.innerWidth
+        window.outerHeight = window.innerHeight + windowFrame
+    } catch (err) { }
+}
+"""
+    )
+
+ +
+ + + +
+
+
#   + + + def + stealth(page) -> None: +
+ +
+ View Source +
def stealth(page) -> None:
+    # chrome_runtime(page)
+    console_debug(page)
+    iframe_content_window(page)
+    # navigator_languages(page)
+    navigator_permissions(page)
+    navigator_plugins(page)
+    navigator_webdriver(page)
+    # navigator_vendor(page)
+    user_agent(page)
+    webgl_vendor(page)
+    window_outerdimensions(page)
+    media_codecs(page)
+
+ +
+ + + +
+
+ + \ No newline at end of file diff --git a/docs/TikTokApi/exceptions.html b/docs/TikTokApi/exceptions.html new file mode 100644 index 00000000..b82f0a15 --- /dev/null +++ b/docs/TikTokApi/exceptions.html @@ -0,0 +1,654 @@ + + + + + + + TikTokApi.exceptions API documentation + + + + + + + + +
+
+

+TikTokApi.exceptions

+ + +
+ View Source +
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)
+
+
+# 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 TikTokNotFoundError(Exception):
+    def __init__(self, message="The requested object does not exists"):
+        self.message = message
+        super().__init__(self.message)
+
+
+class EmptyResponseError(Exception):
+    def __init__(self, message="TikTok sent no data back"):
+        self.message = message
+        super().__init__(self.message)
+
+
+class JSONDecodeFailure(Exception):
+    def __init__(self, message="TikTok sent invalid JSON back"):
+        self.message = message
+        super().__init__(self.message)
+
+
+class TikTokNotAvailableError(Exception):
+    def __init__(self, message="The requested object is not available in this region"):
+        self.message = message
+        super().__init__(self.message)
+
+ +
+ +
+
+
+ #   + + + class + TikTokCaptchaError(builtins.Exception): +
+ +
+ View Source +
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)
+
+ +
+ +

Common base class for all non-exit exceptions.

+
+ + +
+
#   + + + TikTokCaptchaError( + message='TikTok blocks this request displaying a Captcha \nTip: Consider using a proxy or a custom_verify_fp as method parameters' +) +
+ +
+ View Source +
    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)
+
+ +
+ + + +
+
+
Inherited Members
+
+
builtins.BaseException
+
with_traceback
+
args
+ +
+
+
+
+
+
+ #   + + + class + GenericTikTokError(builtins.Exception): +
+ +
+ View Source +
class GenericTikTokError(Exception):
+    def __init__(self, message):
+        self.message = message
+        super().__init__(self.message)
+
+ +
+ +

Common base class for all non-exit exceptions.

+
+ + +
+
#   + + + GenericTikTokError(message) +
+ +
+ View Source +
    def __init__(self, message):
+        self.message = message
+        super().__init__(self.message)
+
+ +
+ + + +
+
+
Inherited Members
+
+
builtins.BaseException
+
with_traceback
+
args
+ +
+
+
+
+
+
+ #   + + + class + TikTokNotFoundError(builtins.Exception): +
+ +
+ View Source +
class TikTokNotFoundError(Exception):
+    def __init__(self, message="The requested object does not exists"):
+        self.message = message
+        super().__init__(self.message)
+
+ +
+ +

Common base class for all non-exit exceptions.

+
+ + +
+
#   + + + TikTokNotFoundError(message='The requested object does not exists') +
+ +
+ View Source +
    def __init__(self, message="The requested object does not exists"):
+        self.message = message
+        super().__init__(self.message)
+
+ +
+ + + +
+
+
Inherited Members
+
+
builtins.BaseException
+
with_traceback
+
args
+ +
+
+
+
+
+
+ #   + + + class + EmptyResponseError(builtins.Exception): +
+ +
+ View Source +
class EmptyResponseError(Exception):
+    def __init__(self, message="TikTok sent no data back"):
+        self.message = message
+        super().__init__(self.message)
+
+ +
+ +

Common base class for all non-exit exceptions.

+
+ + +
+
#   + + + EmptyResponseError(message='TikTok sent no data back') +
+ +
+ View Source +
    def __init__(self, message="TikTok sent no data back"):
+        self.message = message
+        super().__init__(self.message)
+
+ +
+ + + +
+
+
Inherited Members
+
+
builtins.BaseException
+
with_traceback
+
args
+ +
+
+
+
+
+
+ #   + + + class + JSONDecodeFailure(builtins.Exception): +
+ +
+ View Source +
class JSONDecodeFailure(Exception):
+    def __init__(self, message="TikTok sent invalid JSON back"):
+        self.message = message
+        super().__init__(self.message)
+
+ +
+ +

Common base class for all non-exit exceptions.

+
+ + +
+
#   + + + JSONDecodeFailure(message='TikTok sent invalid JSON back') +
+ +
+ View Source +
    def __init__(self, message="TikTok sent invalid JSON back"):
+        self.message = message
+        super().__init__(self.message)
+
+ +
+ + + +
+
+
Inherited Members
+
+
builtins.BaseException
+
with_traceback
+
args
+ +
+
+
+
+
+
+ #   + + + class + TikTokNotAvailableError(builtins.Exception): +
+ +
+ View Source +
class TikTokNotAvailableError(Exception):
+    def __init__(self, message="The requested object is not available in this region"):
+        self.message = message
+        super().__init__(self.message)
+
+ +
+ +

Common base class for all non-exit exceptions.

+
+ + +
+
#   + + + TikTokNotAvailableError(message='The requested object is not available in this region') +
+ +
+ View Source +
    def __init__(self, message="The requested object is not available in this region"):
+        self.message = message
+        super().__init__(self.message)
+
+ +
+ + + +
+
+
Inherited Members
+
+
builtins.BaseException
+
with_traceback
+
args
+ +
+
+
+
+
+ + \ No newline at end of file diff --git a/docs/TikTokApi/helpers.html b/docs/TikTokApi/helpers.html new file mode 100644 index 00000000..41dfd989 --- /dev/null +++ b/docs/TikTokApi/helpers.html @@ -0,0 +1,348 @@ + + + + + + + TikTokApi.helpers API documentation + + + + + + + + +
+
+

+TikTokApi.helpers

+ + +
+ View Source +
from TikTokApi.browser_utilities.browser import browser
+from urllib.parse import quote, urlencode
+from .exceptions import *
+
+import re
+import requests
+
+
+def extract_tag_contents(html):
+    next_json = re.search(
+        r"id=\"__NEXT_DATA__\"\s+type=\"application\/json\"\s*[^>]+>\s*(?P<next_data>[^<]+)",
+        html,
+    )
+    if next_json:
+        nonce_start = '<head nonce="'
+        nonce_end = '">'
+        nonce = html.split(nonce_start)[1].split(nonce_end)[0]
+        j_raw = html.split(
+            '<script id="__NEXT_DATA__" type="application/json" nonce="%s" crossorigin="anonymous">'
+            % nonce
+        )[1].split("</script>")[0]
+        return j_raw
+    else:
+        sigi_json = re.search(
+            r'>\s*window\[[\'"]SIGI_STATE[\'"]\]\s*=\s*(?P<sigi_state>{.+});', html
+        )
+        if sigi_json:
+            return sigi_json.group(1)
+        else:
+            raise TikTokCaptchaError()
+
+
+def extract_video_id_from_url(url):
+    url = requests.head(url=url, allow_redirects=True).url
+    if "@" in url and "/video/" in url:
+        return url.split("/video/")[1].split("?")[0]
+    else:
+        raise TypeError(
+            "URL format not supported. Below is an example of a supported url.\n"
+            "https://www.tiktok.com/@therock/video/6829267836783971589"
+        )
+
+ +
+ +
+
+
#   + + + def + extract_tag_contents(html): +
+ +
+ View Source +
def extract_tag_contents(html):
+    next_json = re.search(
+        r"id=\"__NEXT_DATA__\"\s+type=\"application\/json\"\s*[^>]+>\s*(?P<next_data>[^<]+)",
+        html,
+    )
+    if next_json:
+        nonce_start = '<head nonce="'
+        nonce_end = '">'
+        nonce = html.split(nonce_start)[1].split(nonce_end)[0]
+        j_raw = html.split(
+            '<script id="__NEXT_DATA__" type="application/json" nonce="%s" crossorigin="anonymous">'
+            % nonce
+        )[1].split("</script>")[0]
+        return j_raw
+    else:
+        sigi_json = re.search(
+            r'>\s*window\[[\'"]SIGI_STATE[\'"]\]\s*=\s*(?P<sigi_state>{.+});', html
+        )
+        if sigi_json:
+            return sigi_json.group(1)
+        else:
+            raise TikTokCaptchaError()
+
+ +
+ + + +
+
+
#   + + + def + extract_video_id_from_url(url): +
+ +
+ View Source +
def extract_video_id_from_url(url):
+    url = requests.head(url=url, allow_redirects=True).url
+    if "@" in url and "/video/" in url:
+        return url.split("/video/")[1].split("?")[0]
+    else:
+        raise TypeError(
+            "URL format not supported. Below is an example of a supported url.\n"
+            "https://www.tiktok.com/@therock/video/6829267836783971589"
+        )
+
+ +
+ + + +
+
+ + \ No newline at end of file diff --git a/docs/TikTokApi/tiktok.html b/docs/TikTokApi/tiktok.html new file mode 100644 index 00000000..30ed8af0 --- /dev/null +++ b/docs/TikTokApi/tiktok.html @@ -0,0 +1,3130 @@ + + + + + + + TikTokApi.tiktok API documentation + + + + + + + + +
+
+

+TikTokApi.tiktok

+ + +
+ View Source +
import json
+import logging
+import os
+import random
+import string
+import time
+from typing import ClassVar
+from urllib.parse import quote, urlencode
+
+import requests
+from .api.sound import Sound
+from .api.user import User
+from .api.search import Search
+from .api.hashtag import Hashtag
+from .api.video import Video
+from .api.trending import Trending
+
+from playwright.sync_api import sync_playwright
+
+from .exceptions import *
+from .utilities import LOGGER_NAME, update_messager
+from .browser_utilities.browser import browser
+
+os.environ["no_proxy"] = "127.0.0.1,localhost"
+
+BASE_URL = "https://m.tiktok.com/"
+DESKTOP_BASE_URL = "https://www.tiktok.com/"
+
+
+class TikTokApi:
+    _instance = None
+    logger: ClassVar[logging.Logger] = logging.getLogger(LOGGER_NAME)
+
+    user = User
+    search = Search
+    sound = Sound
+    hashtag = Hashtag
+    video = Video
+    trending = Trending
+
+    @staticmethod
+    def __new__(cls, logging_level=logging.WARNING, *args, **kwargs):
+        """The TikTokApi class. Used to interact with TikTok. This is a singleton
+            class to prevent issues from arising with playwright
+
+        ##### Parameters
+        * logging_level: The logging level you want the program to run at, optional
+            These are the standard python logging module's levels.
+
+        * request_delay: The amount of time in seconds to wait before making a request, optional
+            This is used to throttle your own requests as you may end up making too
+            many requests to TikTok for your IP.
+
+        * custom_device_id: A TikTok parameter needed to download videos, optional
+            The code generates these and handles these pretty well itself, however
+            for some things such as video download you will need to set a consistent
+            one of these. All the methods take this as a optional parameter, however
+            it's cleaner code to store this at the instance level. You can override
+            this at the specific methods.
+
+        * generate_static_device_id: A parameter that generates a custom_device_id at the instance level
+            Use this if you want to download videos from a script but don't want to generate
+            your own custom_device_id parameter.
+
+        * custom_verify_fp: A TikTok parameter needed to work most of the time, optional
+            To get this parameter look at [this video](https://youtu.be/zwLmLfVI-VQ?t=117)
+            I recommend watching the entire thing, as it will help setup this package. All
+            the methods take this as a optional parameter, however it's cleaner code
+            to store this at the instance level. You can override this at the specific
+            methods.
+
+            You can use the following to generate `"".join(random.choice(string.digits)
+            for num in range(19))`
+
+        * use_test_endpoints: Send requests to TikTok's test endpoints, optional
+            This parameter when set to true will make requests to TikTok's testing
+            endpoints instead of the live site. I can't guarantee this will work
+            in the future, however currently basically any custom_verify_fp will
+            work here which is helpful.
+
+        * proxy: A string containing your proxy address, optional
+            If you want to do a lot of scraping of TikTok endpoints you'll likely
+            need a proxy.
+
+            Ex: "https://0.0.0.0:8080"
+
+            All the methods take this as a optional parameter, however it's cleaner code
+            to store this at the instance level. You can override this at the specific
+            methods.
+
+        * use_selenium: Option to use selenium over playwright, optional
+            Playwright is selected by default and is the one that I'm designing the
+            package to be compatable for, however if playwright doesn't work on
+            your machine feel free to set this to True.
+
+        * executablePath: The location of the driver, optional
+            This shouldn't be needed if you're using playwright
+
+        * **kwargs
+            Parameters that are passed on to basically every module and methods
+            that interact with this main class. These may or may not be documented
+            in other places.
+        """
+
+        if cls._instance is None:
+            cls._instance = super(TikTokApi, cls).__new__(cls)
+            cls._instance._initialize(logging_level=logging_level, *args, **kwargs)
+        return cls._instance
+
+    def _initialize(self, logging_level=logging.WARNING, **kwargs):
+        # Add classes from the api folder
+        User.parent = self
+        Search.parent = self
+        Sound.parent = self
+        Hashtag.parent = self
+        Video.parent = self
+        Trending.parent = self
+
+        self.logger.setLevel(level=logging_level)
+
+        # Some Instance Vars
+        self.executablePath = kwargs.get("executablePath", 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.proxy = kwargs.get("proxy", None)
+        self.custom_verify_fp = kwargs.get("custom_verify_fp")
+        self.signer_url = kwargs.get("external_signer", None)
+        self.request_delay = kwargs.get("request_delay", None)
+        self.requests_extra_kwargs = kwargs.get("requests_extra_kwargs", {})
+
+        if kwargs.get("use_test_endpoints", False):
+            global BASE_URL
+            BASE_URL = "https://t.tiktok.com/"
+
+        if kwargs.get("generate_static_device_id", False):
+            self.custom_device_id = "".join(
+                random.choice(string.digits) for num in range(19)
+            )
+
+        if self.signer_url is None:
+            self.browser = browser(**kwargs)
+            self.user_agent = self.browser.user_agent
+
+        try:
+            self.timezone_name = self.__format_new_params(self.browser.timezone_name)
+            self.browser_language = self.__format_new_params(
+                self.browser.browser_language
+            )
+            self.width = self.browser.width
+            self.height = self.browser.height
+            self.region = self.browser.region
+            self.language = self.browser.language
+        except Exception as e:
+            self.logger.exception(
+                "An error occurred while opening your browser, but it was ignored\n",
+                "Are you sure you ran python -m playwright install?",
+            )
+
+            self.timezone_name = ""
+            self.browser_language = ""
+            self.width = "1920"
+            self.height = "1080"
+            self.region = "US"
+            self.language = "en"
+
+    def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict:
+        """Makes requests to TikTok and returns their JSON.
+
+        This is all handled by the package so it's unlikely
+        you will need to use this.
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+        if self.request_delay is not None:
+            time.sleep(self.request_delay)
+
+        if self.proxy is not None:
+            proxy = self.proxy
+
+        if kwargs.get("custom_verify_fp") == None:
+            if self.custom_verify_fp != None:
+                verifyFp = self.custom_verify_fp
+            else:
+                verifyFp = "verify_khr3jabg_V7ucdslq_Vrw9_4KPb_AJ1b_Ks706M8zIJTq"
+        else:
+            verifyFp = kwargs.get("custom_verify_fp")
+
+        tt_params = None
+        send_tt_params = kwargs.get("send_tt_params", False)
+
+        if use_desktop_base_url:
+            full_url = DESKTOP_BASE_URL + path
+        else:
+            full_url = BASE_URL + path
+
+        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
+            )
+            user_agent = self.browser.user_agent
+            referrer = self.browser.referrer
+        else:
+            (
+                verify_fp,
+                device_id,
+                signature,
+                user_agent,
+                referrer,
+            ) = self.external_signer(
+                full_url,
+                custom_device_id=kwargs.get("custom_device_id"),
+                verifyFp=kwargs.get("custom_verify_fp", verifyFp),
+            )
+
+        if not kwargs.get("send_tt_params", False):
+            tt_params = None
+
+        query = {"verifyFp": verify_fp, "device_id": device_id, "_signature": signature}
+        url = "{}&{}".format(full_url, urlencode(query))
+
+        h = requests.head(
+            url,
+            headers={"x-secsdk-csrf-version": "1.2.5", "x-secsdk-csrf-request": "1"},
+            proxies=self._format_proxy(proxy),
+            **self.requests_extra_kwargs,
+        )
+
+        csrf_token = None
+        if not use_desktop_base_url:
+            csrf_session_id = h.cookies["csrf_session_id"]
+            csrf_token = h.headers["X-Ware-Csrf-Token"].split(",")[1]
+            kwargs["csrf_session_id"] = csrf_session_id
+
+        headers = {
+            "authority": "m.tiktok.com",
+            "method": "GET",
+            "path": url.split("tiktok.com")[1],
+            "scheme": "https",
+            "accept": "application/json, text/plain, */*",
+            "accept-encoding": "gzip",
+            "accept-language": "en-US,en;q=0.9",
+            "origin": referrer,
+            "referer": referrer,
+            "sec-fetch-dest": "empty",
+            "sec-fetch-mode": "cors",
+            "sec-fetch-site": "none",
+            "sec-gpc": "1",
+            "user-agent": user_agent,
+            "x-secsdk-csrf-token": csrf_token,
+            "x-tt-params": tt_params,
+        }
+
+        self.logger.info(f"GET: %s\n\theaders: %s", url, headers)
+        r = requests.get(
+            url,
+            headers=headers,
+            cookies=self._get_cookies(**kwargs),
+            proxies=self._format_proxy(proxy),
+            **self.requests_extra_kwargs,
+        )
+
+        try:
+            json = r.json()
+            if (
+                json.get("type") == "verify"
+                or json.get("verifyConfig", {}).get("type", "") == "verify"
+            ):
+                self.logger.error(
+                    "Tiktok wants to display a captcha.\nResponse:\n%s\nCookies:\n%s",
+                    r.text,
+                    self._get_cookies(**kwargs),
+                )
+                raise TikTokCaptchaError()
+
+            # statusCode from props->pageProps->statusCode thanks @adiantek on #403
+            error_codes = {
+                "0": "OK",
+                "450": "CLIENT_PAGE_ERROR",
+                "10000": "VERIFY_CODE",
+                "10101": "SERVER_ERROR_NOT_500",
+                "10102": "USER_NOT_LOGIN",
+                "10111": "NET_ERROR",
+                "10113": "SHARK_SLIDE",
+                "10114": "SHARK_BLOCK",
+                "10119": "LIVE_NEED_LOGIN",
+                "10202": "USER_NOT_EXIST",
+                "10203": "MUSIC_NOT_EXIST",
+                "10204": "VIDEO_NOT_EXIST",
+                "10205": "HASHTAG_NOT_EXIST",
+                "10208": "EFFECT_NOT_EXIST",
+                "10209": "HASHTAG_BLACK_LIST",
+                "10210": "LIVE_NOT_EXIST",
+                "10211": "HASHTAG_SENSITIVITY_WORD",
+                "10212": "HASHTAG_UNSHELVE",
+                "10213": "VIDEO_LOW_AGE_M",
+                "10214": "VIDEO_LOW_AGE_T",
+                "10215": "VIDEO_ABNORMAL",
+                "10216": "VIDEO_PRIVATE_BY_USER",
+                "10217": "VIDEO_FIRST_REVIEW_UNSHELVE",
+                "10218": "MUSIC_UNSHELVE",
+                "10219": "MUSIC_NO_COPYRIGHT",
+                "10220": "VIDEO_UNSHELVE_BY_MUSIC",
+                "10221": "USER_BAN",
+                "10222": "USER_PRIVATE",
+                "10223": "USER_FTC",
+                "10224": "GAME_NOT_EXIST",
+                "10225": "USER_UNIQUE_SENSITIVITY",
+                "10227": "VIDEO_NEED_RECHECK",
+                "10228": "VIDEO_RISK",
+                "10229": "VIDEO_R_MASK",
+                "10230": "VIDEO_RISK_MASK",
+                "10231": "VIDEO_GEOFENCE_BLOCK",
+                "10404": "FYP_VIDEO_LIST_LIMIT",
+                "undefined": "MEDIA_ERROR",
+            }
+            statusCode = json.get("statusCode", 0)
+            self.logger.info(f"TikTok Returned: %s", json)
+            if statusCode == 10201:
+                # Invalid Entity
+                raise TikTokNotFoundError(
+                    "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")
+            elif statusCode != 0 and statusCode != -1:
+                raise GenericTikTokError(
+                    error_codes.get(
+                        statusCode, f"TikTok sent an unknown StatusCode of {statusCode}"
+                    )
+                )
+
+            return r.json()
+        except ValueError as e:
+            text = r.text
+            self.logger.info("TikTok response: %s", text)
+            if len(text) == 0:
+                raise EmptyResponseError(
+                    "Empty response from Tiktok to " + url
+                ) from None
+            else:
+                self.logger.exception("Converting response to JSON failed")
+                raise JSONDecodeFailure() from e
+
+    def __del__(self):
+        """A basic cleanup method, called automatically from the code"""
+        try:
+            self.browser._clean_up()
+        except Exception:
+            pass
+        try:
+            get_playwright().stop()
+        except Exception:
+            pass
+        TikTokApi._instance = None
+
+    def external_signer(self, url, custom_device_id=None, verifyFp=None):
+        """Makes requests to an external signer instead of using a browser.
+
+        ##### Parameters
+        * url: The server to make requests to
+            This server is designed to sign requests. You can find an example
+            of this signature server in the examples folder.
+
+        * custom_device_id: A TikTok parameter needed to download videos
+            The code generates these and handles these pretty well itself, however
+            for some things such as video download you will need to set a consistent
+            one of these.
+
+        * custom_verify_fp: A TikTok parameter needed to work most of the time,
+            To get this parameter look at [this video](https://youtu.be/zwLmLfVI-VQ?t=117)
+            I recommend watching the entire thing, as it will help setup this package.
+        """
+        if custom_device_id is not None:
+            query = {
+                "url": url,
+                "custom_device_id": custom_device_id,
+                "verifyFp": verifyFp,
+            }
+        else:
+            query = {"url": url, "verifyFp": verifyFp}
+        data = requests.get(
+            self.signer_url + "?{}".format(urlencode(query)),
+            **self.requests_extra_kwargs,
+        )
+        parsed_data = data.json()
+
+        return (
+            parsed_data["verifyFp"],
+            parsed_data["device_id"],
+            parsed_data["_signature"],
+            parsed_data["user_agent"],
+            parsed_data["referrer"],
+        )
+
+    def _get_cookies(self, **kwargs):
+        """Extracts cookies from the kwargs passed to the function for get_data"""
+        device_id = kwargs.get(
+            "custom_device_id",
+            "".join(random.choice(string.digits) for num in range(19)),
+        )
+        if kwargs.get("custom_verify_fp") is None:
+            if self.custom_verify_fp is not None:
+                verifyFp = self.custom_verify_fp
+            else:
+                verifyFp = None
+        else:
+            verifyFp = kwargs.get("custom_verify_fp")
+
+        if kwargs.get("force_verify_fp_on_cookie_header", False):
+            return {
+                "tt_webid": device_id,
+                "tt_webid_v2": device_id,
+                "csrf_session_id": kwargs.get("csrf_session_id"),
+                "tt_csrf_token": "".join(
+                    random.choice(string.ascii_uppercase + string.ascii_lowercase)
+                    for i in range(16)
+                ),
+                "s_v_web_id": verifyFp,
+                "ttwid": kwargs.get("ttwid"),
+            }
+        else:
+            return {
+                "tt_webid": device_id,
+                "tt_webid_v2": device_id,
+                "csrf_session_id": kwargs.get("csrf_session_id"),
+                "tt_csrf_token": "".join(
+                    random.choice(string.ascii_uppercase + string.ascii_lowercase)
+                    for i in range(16)
+                ),
+                "ttwid": kwargs.get("ttwid"),
+            }
+
+    def get_bytes(self, **kwargs) -> bytes:
+        """Returns TikTok's response as bytes, similar to get_data"""
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+        if self.signer_url is None:
+            verify_fp, device_id, signature, _ = self.browser.sign_url(
+                calc_tt_params=False, **kwargs
+            )
+            user_agent = self.browser.user_agent
+            referrer = self.browser.referrer
+        else:
+            (
+                verify_fp,
+                device_id,
+                signature,
+                user_agent,
+                referrer,
+            ) = self.external_signer(
+                kwargs["url"], custom_device_id=kwargs.get("custom_device_id", None)
+            )
+        query = {"verifyFp": verify_fp, "_signature": signature}
+        url = "{}&{}".format(kwargs["url"], urlencode(query))
+        r = requests.get(
+            url,
+            headers={
+                "Accept": "*/*",
+                "Accept-Encoding": "identity;q=1, *;q=0",
+                "Accept-Language": "en-US;en;q=0.9",
+                "Cache-Control": "no-cache",
+                "Connection": "keep-alive",
+                "Host": url.split("/")[2],
+                "Pragma": "no-cache",
+                "Range": "bytes=0-",
+                "Referer": "https://www.tiktok.com/",
+                "User-Agent": user_agent,
+            },
+            proxies=self._format_proxy(proxy),
+            cookies=self._get_cookies(**kwargs),
+        )
+        return r.content
+
+    @staticmethod
+    def generate_device_id():
+        """Generates a valid device_id for other methods. Pass this as the custom_device_id field to download videos"""
+        return "".join([random.choice(string.digits) for num in range(19)])
+
+    #
+    # PRIVATE METHODS
+    #
+
+    def _format_proxy(self, proxy) -> dict:
+        """
+        Formats the proxy object
+        """
+        if proxy is None and self.proxy is not None:
+            proxy = self.proxy
+        if proxy is not None:
+            return {"http": proxy, "https": proxy}
+        else:
+            return None
+
+    # Process the kwargs
+    def _process_kwargs(self, kwargs):
+        region = kwargs.get("region", "US")
+        language = kwargs.get("language", "en")
+        proxy = kwargs.get("proxy", None)
+        maxCount = kwargs.get("maxCount", 35)
+
+        if kwargs.get("custom_device_id", None) != None:
+            device_id = kwargs.get("custom_device_id")
+        else:
+            if self.custom_device_id != None:
+                device_id = self.custom_device_id
+            else:
+                device_id = "".join(random.choice(string.digits) for num in range(19))
+        return region, language, proxy, maxCount, device_id
+
+    def __get_js(self, proxy=None) -> str:
+        return requests.get(
+            "https://sf16-muse-va.ibytedtos.com/obj/rc-web-sdk-gcs/acrawler.js",
+            proxies=self._format_proxy(proxy),
+            **self.requests_extra_kwargs,
+        ).text
+
+    def __format_new_params(self, parm) -> str:
+        # TODO: Maybe try not doing this? It should be handled by the urlencode.
+        return parm.replace("/", "%2F").replace(" ", "+").replace(";", "%3B")
+
+    def _add_url_params(self) -> str:
+        query = {
+            "aid": 1988,
+            "app_name": "tiktok_web",
+            "device_platform": "web_mobile",
+            "region": self.region or "US",
+            "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_platform": "iPhone",
+            "browser_name": "Mozilla",
+            "browser_version": self.user_agent,
+            "browser_online": "true",
+            "timezone_name": self.timezone_name or "America/Chicago",
+            "is_page_visible": "true",
+            "focus_state": "true",
+            "is_fullscreen": "false",
+            "history_len": random.randint(0, 30),
+            "language": self.language or "en",
+        }
+
+        return urlencode(query)
+
+ +
+ +
+
+
+ #   + + + class + TikTokApi: +
+ +
+ View Source +
class TikTokApi:
+    _instance = None
+    logger: ClassVar[logging.Logger] = logging.getLogger(LOGGER_NAME)
+
+    user = User
+    search = Search
+    sound = Sound
+    hashtag = Hashtag
+    video = Video
+    trending = Trending
+
+    @staticmethod
+    def __new__(cls, logging_level=logging.WARNING, *args, **kwargs):
+        """The TikTokApi class. Used to interact with TikTok. This is a singleton
+            class to prevent issues from arising with playwright
+
+        ##### Parameters
+        * logging_level: The logging level you want the program to run at, optional
+            These are the standard python logging module's levels.
+
+        * request_delay: The amount of time in seconds to wait before making a request, optional
+            This is used to throttle your own requests as you may end up making too
+            many requests to TikTok for your IP.
+
+        * custom_device_id: A TikTok parameter needed to download videos, optional
+            The code generates these and handles these pretty well itself, however
+            for some things such as video download you will need to set a consistent
+            one of these. All the methods take this as a optional parameter, however
+            it's cleaner code to store this at the instance level. You can override
+            this at the specific methods.
+
+        * generate_static_device_id: A parameter that generates a custom_device_id at the instance level
+            Use this if you want to download videos from a script but don't want to generate
+            your own custom_device_id parameter.
+
+        * custom_verify_fp: A TikTok parameter needed to work most of the time, optional
+            To get this parameter look at [this video](https://youtu.be/zwLmLfVI-VQ?t=117)
+            I recommend watching the entire thing, as it will help setup this package. All
+            the methods take this as a optional parameter, however it's cleaner code
+            to store this at the instance level. You can override this at the specific
+            methods.
+
+            You can use the following to generate `"".join(random.choice(string.digits)
+            for num in range(19))`
+
+        * use_test_endpoints: Send requests to TikTok's test endpoints, optional
+            This parameter when set to true will make requests to TikTok's testing
+            endpoints instead of the live site. I can't guarantee this will work
+            in the future, however currently basically any custom_verify_fp will
+            work here which is helpful.
+
+        * proxy: A string containing your proxy address, optional
+            If you want to do a lot of scraping of TikTok endpoints you'll likely
+            need a proxy.
+
+            Ex: "https://0.0.0.0:8080"
+
+            All the methods take this as a optional parameter, however it's cleaner code
+            to store this at the instance level. You can override this at the specific
+            methods.
+
+        * use_selenium: Option to use selenium over playwright, optional
+            Playwright is selected by default and is the one that I'm designing the
+            package to be compatable for, however if playwright doesn't work on
+            your machine feel free to set this to True.
+
+        * executablePath: The location of the driver, optional
+            This shouldn't be needed if you're using playwright
+
+        * **kwargs
+            Parameters that are passed on to basically every module and methods
+            that interact with this main class. These may or may not be documented
+            in other places.
+        """
+
+        if cls._instance is None:
+            cls._instance = super(TikTokApi, cls).__new__(cls)
+            cls._instance._initialize(logging_level=logging_level, *args, **kwargs)
+        return cls._instance
+
+    def _initialize(self, logging_level=logging.WARNING, **kwargs):
+        # Add classes from the api folder
+        User.parent = self
+        Search.parent = self
+        Sound.parent = self
+        Hashtag.parent = self
+        Video.parent = self
+        Trending.parent = self
+
+        self.logger.setLevel(level=logging_level)
+
+        # Some Instance Vars
+        self.executablePath = kwargs.get("executablePath", 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.proxy = kwargs.get("proxy", None)
+        self.custom_verify_fp = kwargs.get("custom_verify_fp")
+        self.signer_url = kwargs.get("external_signer", None)
+        self.request_delay = kwargs.get("request_delay", None)
+        self.requests_extra_kwargs = kwargs.get("requests_extra_kwargs", {})
+
+        if kwargs.get("use_test_endpoints", False):
+            global BASE_URL
+            BASE_URL = "https://t.tiktok.com/"
+
+        if kwargs.get("generate_static_device_id", False):
+            self.custom_device_id = "".join(
+                random.choice(string.digits) for num in range(19)
+            )
+
+        if self.signer_url is None:
+            self.browser = browser(**kwargs)
+            self.user_agent = self.browser.user_agent
+
+        try:
+            self.timezone_name = self.__format_new_params(self.browser.timezone_name)
+            self.browser_language = self.__format_new_params(
+                self.browser.browser_language
+            )
+            self.width = self.browser.width
+            self.height = self.browser.height
+            self.region = self.browser.region
+            self.language = self.browser.language
+        except Exception as e:
+            self.logger.exception(
+                "An error occurred while opening your browser, but it was ignored\n",
+                "Are you sure you ran python -m playwright install?",
+            )
+
+            self.timezone_name = ""
+            self.browser_language = ""
+            self.width = "1920"
+            self.height = "1080"
+            self.region = "US"
+            self.language = "en"
+
+    def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict:
+        """Makes requests to TikTok and returns their JSON.
+
+        This is all handled by the package so it's unlikely
+        you will need to use this.
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+        if self.request_delay is not None:
+            time.sleep(self.request_delay)
+
+        if self.proxy is not None:
+            proxy = self.proxy
+
+        if kwargs.get("custom_verify_fp") == None:
+            if self.custom_verify_fp != None:
+                verifyFp = self.custom_verify_fp
+            else:
+                verifyFp = "verify_khr3jabg_V7ucdslq_Vrw9_4KPb_AJ1b_Ks706M8zIJTq"
+        else:
+            verifyFp = kwargs.get("custom_verify_fp")
+
+        tt_params = None
+        send_tt_params = kwargs.get("send_tt_params", False)
+
+        if use_desktop_base_url:
+            full_url = DESKTOP_BASE_URL + path
+        else:
+            full_url = BASE_URL + path
+
+        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
+            )
+            user_agent = self.browser.user_agent
+            referrer = self.browser.referrer
+        else:
+            (
+                verify_fp,
+                device_id,
+                signature,
+                user_agent,
+                referrer,
+            ) = self.external_signer(
+                full_url,
+                custom_device_id=kwargs.get("custom_device_id"),
+                verifyFp=kwargs.get("custom_verify_fp", verifyFp),
+            )
+
+        if not kwargs.get("send_tt_params", False):
+            tt_params = None
+
+        query = {"verifyFp": verify_fp, "device_id": device_id, "_signature": signature}
+        url = "{}&{}".format(full_url, urlencode(query))
+
+        h = requests.head(
+            url,
+            headers={"x-secsdk-csrf-version": "1.2.5", "x-secsdk-csrf-request": "1"},
+            proxies=self._format_proxy(proxy),
+            **self.requests_extra_kwargs,
+        )
+
+        csrf_token = None
+        if not use_desktop_base_url:
+            csrf_session_id = h.cookies["csrf_session_id"]
+            csrf_token = h.headers["X-Ware-Csrf-Token"].split(",")[1]
+            kwargs["csrf_session_id"] = csrf_session_id
+
+        headers = {
+            "authority": "m.tiktok.com",
+            "method": "GET",
+            "path": url.split("tiktok.com")[1],
+            "scheme": "https",
+            "accept": "application/json, text/plain, */*",
+            "accept-encoding": "gzip",
+            "accept-language": "en-US,en;q=0.9",
+            "origin": referrer,
+            "referer": referrer,
+            "sec-fetch-dest": "empty",
+            "sec-fetch-mode": "cors",
+            "sec-fetch-site": "none",
+            "sec-gpc": "1",
+            "user-agent": user_agent,
+            "x-secsdk-csrf-token": csrf_token,
+            "x-tt-params": tt_params,
+        }
+
+        self.logger.info(f"GET: %s\n\theaders: %s", url, headers)
+        r = requests.get(
+            url,
+            headers=headers,
+            cookies=self._get_cookies(**kwargs),
+            proxies=self._format_proxy(proxy),
+            **self.requests_extra_kwargs,
+        )
+
+        try:
+            json = r.json()
+            if (
+                json.get("type") == "verify"
+                or json.get("verifyConfig", {}).get("type", "") == "verify"
+            ):
+                self.logger.error(
+                    "Tiktok wants to display a captcha.\nResponse:\n%s\nCookies:\n%s",
+                    r.text,
+                    self._get_cookies(**kwargs),
+                )
+                raise TikTokCaptchaError()
+
+            # statusCode from props->pageProps->statusCode thanks @adiantek on #403
+            error_codes = {
+                "0": "OK",
+                "450": "CLIENT_PAGE_ERROR",
+                "10000": "VERIFY_CODE",
+                "10101": "SERVER_ERROR_NOT_500",
+                "10102": "USER_NOT_LOGIN",
+                "10111": "NET_ERROR",
+                "10113": "SHARK_SLIDE",
+                "10114": "SHARK_BLOCK",
+                "10119": "LIVE_NEED_LOGIN",
+                "10202": "USER_NOT_EXIST",
+                "10203": "MUSIC_NOT_EXIST",
+                "10204": "VIDEO_NOT_EXIST",
+                "10205": "HASHTAG_NOT_EXIST",
+                "10208": "EFFECT_NOT_EXIST",
+                "10209": "HASHTAG_BLACK_LIST",
+                "10210": "LIVE_NOT_EXIST",
+                "10211": "HASHTAG_SENSITIVITY_WORD",
+                "10212": "HASHTAG_UNSHELVE",
+                "10213": "VIDEO_LOW_AGE_M",
+                "10214": "VIDEO_LOW_AGE_T",
+                "10215": "VIDEO_ABNORMAL",
+                "10216": "VIDEO_PRIVATE_BY_USER",
+                "10217": "VIDEO_FIRST_REVIEW_UNSHELVE",
+                "10218": "MUSIC_UNSHELVE",
+                "10219": "MUSIC_NO_COPYRIGHT",
+                "10220": "VIDEO_UNSHELVE_BY_MUSIC",
+                "10221": "USER_BAN",
+                "10222": "USER_PRIVATE",
+                "10223": "USER_FTC",
+                "10224": "GAME_NOT_EXIST",
+                "10225": "USER_UNIQUE_SENSITIVITY",
+                "10227": "VIDEO_NEED_RECHECK",
+                "10228": "VIDEO_RISK",
+                "10229": "VIDEO_R_MASK",
+                "10230": "VIDEO_RISK_MASK",
+                "10231": "VIDEO_GEOFENCE_BLOCK",
+                "10404": "FYP_VIDEO_LIST_LIMIT",
+                "undefined": "MEDIA_ERROR",
+            }
+            statusCode = json.get("statusCode", 0)
+            self.logger.info(f"TikTok Returned: %s", json)
+            if statusCode == 10201:
+                # Invalid Entity
+                raise TikTokNotFoundError(
+                    "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")
+            elif statusCode != 0 and statusCode != -1:
+                raise GenericTikTokError(
+                    error_codes.get(
+                        statusCode, f"TikTok sent an unknown StatusCode of {statusCode}"
+                    )
+                )
+
+            return r.json()
+        except ValueError as e:
+            text = r.text
+            self.logger.info("TikTok response: %s", text)
+            if len(text) == 0:
+                raise EmptyResponseError(
+                    "Empty response from Tiktok to " + url
+                ) from None
+            else:
+                self.logger.exception("Converting response to JSON failed")
+                raise JSONDecodeFailure() from e
+
+    def __del__(self):
+        """A basic cleanup method, called automatically from the code"""
+        try:
+            self.browser._clean_up()
+        except Exception:
+            pass
+        try:
+            get_playwright().stop()
+        except Exception:
+            pass
+        TikTokApi._instance = None
+
+    def external_signer(self, url, custom_device_id=None, verifyFp=None):
+        """Makes requests to an external signer instead of using a browser.
+
+        ##### Parameters
+        * url: The server to make requests to
+            This server is designed to sign requests. You can find an example
+            of this signature server in the examples folder.
+
+        * custom_device_id: A TikTok parameter needed to download videos
+            The code generates these and handles these pretty well itself, however
+            for some things such as video download you will need to set a consistent
+            one of these.
+
+        * custom_verify_fp: A TikTok parameter needed to work most of the time,
+            To get this parameter look at [this video](https://youtu.be/zwLmLfVI-VQ?t=117)
+            I recommend watching the entire thing, as it will help setup this package.
+        """
+        if custom_device_id is not None:
+            query = {
+                "url": url,
+                "custom_device_id": custom_device_id,
+                "verifyFp": verifyFp,
+            }
+        else:
+            query = {"url": url, "verifyFp": verifyFp}
+        data = requests.get(
+            self.signer_url + "?{}".format(urlencode(query)),
+            **self.requests_extra_kwargs,
+        )
+        parsed_data = data.json()
+
+        return (
+            parsed_data["verifyFp"],
+            parsed_data["device_id"],
+            parsed_data["_signature"],
+            parsed_data["user_agent"],
+            parsed_data["referrer"],
+        )
+
+    def _get_cookies(self, **kwargs):
+        """Extracts cookies from the kwargs passed to the function for get_data"""
+        device_id = kwargs.get(
+            "custom_device_id",
+            "".join(random.choice(string.digits) for num in range(19)),
+        )
+        if kwargs.get("custom_verify_fp") is None:
+            if self.custom_verify_fp is not None:
+                verifyFp = self.custom_verify_fp
+            else:
+                verifyFp = None
+        else:
+            verifyFp = kwargs.get("custom_verify_fp")
+
+        if kwargs.get("force_verify_fp_on_cookie_header", False):
+            return {
+                "tt_webid": device_id,
+                "tt_webid_v2": device_id,
+                "csrf_session_id": kwargs.get("csrf_session_id"),
+                "tt_csrf_token": "".join(
+                    random.choice(string.ascii_uppercase + string.ascii_lowercase)
+                    for i in range(16)
+                ),
+                "s_v_web_id": verifyFp,
+                "ttwid": kwargs.get("ttwid"),
+            }
+        else:
+            return {
+                "tt_webid": device_id,
+                "tt_webid_v2": device_id,
+                "csrf_session_id": kwargs.get("csrf_session_id"),
+                "tt_csrf_token": "".join(
+                    random.choice(string.ascii_uppercase + string.ascii_lowercase)
+                    for i in range(16)
+                ),
+                "ttwid": kwargs.get("ttwid"),
+            }
+
+    def get_bytes(self, **kwargs) -> bytes:
+        """Returns TikTok's response as bytes, similar to get_data"""
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+        if self.signer_url is None:
+            verify_fp, device_id, signature, _ = self.browser.sign_url(
+                calc_tt_params=False, **kwargs
+            )
+            user_agent = self.browser.user_agent
+            referrer = self.browser.referrer
+        else:
+            (
+                verify_fp,
+                device_id,
+                signature,
+                user_agent,
+                referrer,
+            ) = self.external_signer(
+                kwargs["url"], custom_device_id=kwargs.get("custom_device_id", None)
+            )
+        query = {"verifyFp": verify_fp, "_signature": signature}
+        url = "{}&{}".format(kwargs["url"], urlencode(query))
+        r = requests.get(
+            url,
+            headers={
+                "Accept": "*/*",
+                "Accept-Encoding": "identity;q=1, *;q=0",
+                "Accept-Language": "en-US;en;q=0.9",
+                "Cache-Control": "no-cache",
+                "Connection": "keep-alive",
+                "Host": url.split("/")[2],
+                "Pragma": "no-cache",
+                "Range": "bytes=0-",
+                "Referer": "https://www.tiktok.com/",
+                "User-Agent": user_agent,
+            },
+            proxies=self._format_proxy(proxy),
+            cookies=self._get_cookies(**kwargs),
+        )
+        return r.content
+
+    @staticmethod
+    def generate_device_id():
+        """Generates a valid device_id for other methods. Pass this as the custom_device_id field to download videos"""
+        return "".join([random.choice(string.digits) for num in range(19)])
+
+    #
+    # PRIVATE METHODS
+    #
+
+    def _format_proxy(self, proxy) -> dict:
+        """
+        Formats the proxy object
+        """
+        if proxy is None and self.proxy is not None:
+            proxy = self.proxy
+        if proxy is not None:
+            return {"http": proxy, "https": proxy}
+        else:
+            return None
+
+    # Process the kwargs
+    def _process_kwargs(self, kwargs):
+        region = kwargs.get("region", "US")
+        language = kwargs.get("language", "en")
+        proxy = kwargs.get("proxy", None)
+        maxCount = kwargs.get("maxCount", 35)
+
+        if kwargs.get("custom_device_id", None) != None:
+            device_id = kwargs.get("custom_device_id")
+        else:
+            if self.custom_device_id != None:
+                device_id = self.custom_device_id
+            else:
+                device_id = "".join(random.choice(string.digits) for num in range(19))
+        return region, language, proxy, maxCount, device_id
+
+    def __get_js(self, proxy=None) -> str:
+        return requests.get(
+            "https://sf16-muse-va.ibytedtos.com/obj/rc-web-sdk-gcs/acrawler.js",
+            proxies=self._format_proxy(proxy),
+            **self.requests_extra_kwargs,
+        ).text
+
+    def __format_new_params(self, parm) -> str:
+        # TODO: Maybe try not doing this? It should be handled by the urlencode.
+        return parm.replace("/", "%2F").replace(" ", "+").replace(";", "%3B")
+
+    def _add_url_params(self) -> str:
+        query = {
+            "aid": 1988,
+            "app_name": "tiktok_web",
+            "device_platform": "web_mobile",
+            "region": self.region or "US",
+            "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_platform": "iPhone",
+            "browser_name": "Mozilla",
+            "browser_version": self.user_agent,
+            "browser_online": "true",
+            "timezone_name": self.timezone_name or "America/Chicago",
+            "is_page_visible": "true",
+            "focus_state": "true",
+            "is_fullscreen": "false",
+            "history_len": random.randint(0, 30),
+            "language": self.language or "en",
+        }
+
+        return urlencode(query)
+
+ +
+ + + +
+
#   + +
@staticmethod
+ + TikTokApi(logging_level=30, *args, **kwargs) +
+ +
+ View Source +
    @staticmethod
+    def __new__(cls, logging_level=logging.WARNING, *args, **kwargs):
+        """The TikTokApi class. Used to interact with TikTok. This is a singleton
+            class to prevent issues from arising with playwright
+
+        ##### Parameters
+        * logging_level: The logging level you want the program to run at, optional
+            These are the standard python logging module's levels.
+
+        * request_delay: The amount of time in seconds to wait before making a request, optional
+            This is used to throttle your own requests as you may end up making too
+            many requests to TikTok for your IP.
+
+        * custom_device_id: A TikTok parameter needed to download videos, optional
+            The code generates these and handles these pretty well itself, however
+            for some things such as video download you will need to set a consistent
+            one of these. All the methods take this as a optional parameter, however
+            it's cleaner code to store this at the instance level. You can override
+            this at the specific methods.
+
+        * generate_static_device_id: A parameter that generates a custom_device_id at the instance level
+            Use this if you want to download videos from a script but don't want to generate
+            your own custom_device_id parameter.
+
+        * custom_verify_fp: A TikTok parameter needed to work most of the time, optional
+            To get this parameter look at [this video](https://youtu.be/zwLmLfVI-VQ?t=117)
+            I recommend watching the entire thing, as it will help setup this package. All
+            the methods take this as a optional parameter, however it's cleaner code
+            to store this at the instance level. You can override this at the specific
+            methods.
+
+            You can use the following to generate `"".join(random.choice(string.digits)
+            for num in range(19))`
+
+        * use_test_endpoints: Send requests to TikTok's test endpoints, optional
+            This parameter when set to true will make requests to TikTok's testing
+            endpoints instead of the live site. I can't guarantee this will work
+            in the future, however currently basically any custom_verify_fp will
+            work here which is helpful.
+
+        * proxy: A string containing your proxy address, optional
+            If you want to do a lot of scraping of TikTok endpoints you'll likely
+            need a proxy.
+
+            Ex: "https://0.0.0.0:8080"
+
+            All the methods take this as a optional parameter, however it's cleaner code
+            to store this at the instance level. You can override this at the specific
+            methods.
+
+        * use_selenium: Option to use selenium over playwright, optional
+            Playwright is selected by default and is the one that I'm designing the
+            package to be compatable for, however if playwright doesn't work on
+            your machine feel free to set this to True.
+
+        * executablePath: The location of the driver, optional
+            This shouldn't be needed if you're using playwright
+
+        * **kwargs
+            Parameters that are passed on to basically every module and methods
+            that interact with this main class. These may or may not be documented
+            in other places.
+        """
+
+        if cls._instance is None:
+            cls._instance = super(TikTokApi, cls).__new__(cls)
+            cls._instance._initialize(logging_level=logging_level, *args, **kwargs)
+        return cls._instance
+
+ +
+ +

The TikTokApi class. Used to interact with TikTok. This is a singleton + class to prevent issues from arising with playwright

+ +
Parameters
+ +
    +
  • logging_level: The logging level you want the program to run at, optional +These are the standard python logging module's levels.

  • +
  • request_delay: The amount of time in seconds to wait before making a request, optional +This is used to throttle your own requests as you may end up making too +many requests to TikTok for your IP.

  • +
  • custom_device_id: A TikTok parameter needed to download videos, optional +The code generates these and handles these pretty well itself, however +for some things such as video download you will need to set a consistent +one of these. All the methods take this as a optional parameter, however +it's cleaner code to store this at the instance level. You can override +this at the specific methods.

  • +
  • generate_static_device_id: A parameter that generates a custom_device_id at the instance level +Use this if you want to download videos from a script but don't want to generate +your own custom_device_id parameter.

  • +
  • custom_verify_fp: A TikTok parameter needed to work most of the time, optional +To get this parameter look at this video +I recommend watching the entire thing, as it will help setup this package. All +the methods take this as a optional parameter, however it's cleaner code +to store this at the instance level. You can override this at the specific +methods.

    + +

    You can use the following to generate "".join(random.choice(string.digits) +for num in range(19))

  • +
  • use_test_endpoints: Send requests to TikTok's test endpoints, optional +This parameter when set to true will make requests to TikTok's testing +endpoints instead of the live site. I can't guarantee this will work +in the future, however currently basically any custom_verify_fp will +work here which is helpful.

  • +
  • proxy: A string containing your proxy address, optional +If you want to do a lot of scraping of TikTok endpoints you'll likely +need a proxy.

    + +

    Ex: "https://0.0.0.0:8080"

    + +

    All the methods take this as a optional parameter, however it's cleaner code +to store this at the instance level. You can override this at the specific +methods.

  • +
  • use_selenium: Option to use selenium over playwright, optional +Playwright is selected by default and is the one that I'm designing the +package to be compatable for, however if playwright doesn't work on +your machine feel free to set this to True.

  • +
  • executablePath: The location of the driver, optional +This shouldn't be needed if you're using playwright

  • +
  • **kwargs +Parameters that are passed on to basically every module and methods +that interact with this main class. These may or may not be documented +in other places.

  • +
+
+ + +
+
+
#   + + logger: ClassVar[logging.Logger] = <Logger TikTokApi (WARNING)> +
+ + + +
+
+
#   + + + def + get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict: +
+ +
+ View Source +
    def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict:
+        """Makes requests to TikTok and returns their JSON.
+
+        This is all handled by the package so it's unlikely
+        you will need to use this.
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+        if self.request_delay is not None:
+            time.sleep(self.request_delay)
+
+        if self.proxy is not None:
+            proxy = self.proxy
+
+        if kwargs.get("custom_verify_fp") == None:
+            if self.custom_verify_fp != None:
+                verifyFp = self.custom_verify_fp
+            else:
+                verifyFp = "verify_khr3jabg_V7ucdslq_Vrw9_4KPb_AJ1b_Ks706M8zIJTq"
+        else:
+            verifyFp = kwargs.get("custom_verify_fp")
+
+        tt_params = None
+        send_tt_params = kwargs.get("send_tt_params", False)
+
+        if use_desktop_base_url:
+            full_url = DESKTOP_BASE_URL + path
+        else:
+            full_url = BASE_URL + path
+
+        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
+            )
+            user_agent = self.browser.user_agent
+            referrer = self.browser.referrer
+        else:
+            (
+                verify_fp,
+                device_id,
+                signature,
+                user_agent,
+                referrer,
+            ) = self.external_signer(
+                full_url,
+                custom_device_id=kwargs.get("custom_device_id"),
+                verifyFp=kwargs.get("custom_verify_fp", verifyFp),
+            )
+
+        if not kwargs.get("send_tt_params", False):
+            tt_params = None
+
+        query = {"verifyFp": verify_fp, "device_id": device_id, "_signature": signature}
+        url = "{}&{}".format(full_url, urlencode(query))
+
+        h = requests.head(
+            url,
+            headers={"x-secsdk-csrf-version": "1.2.5", "x-secsdk-csrf-request": "1"},
+            proxies=self._format_proxy(proxy),
+            **self.requests_extra_kwargs,
+        )
+
+        csrf_token = None
+        if not use_desktop_base_url:
+            csrf_session_id = h.cookies["csrf_session_id"]
+            csrf_token = h.headers["X-Ware-Csrf-Token"].split(",")[1]
+            kwargs["csrf_session_id"] = csrf_session_id
+
+        headers = {
+            "authority": "m.tiktok.com",
+            "method": "GET",
+            "path": url.split("tiktok.com")[1],
+            "scheme": "https",
+            "accept": "application/json, text/plain, */*",
+            "accept-encoding": "gzip",
+            "accept-language": "en-US,en;q=0.9",
+            "origin": referrer,
+            "referer": referrer,
+            "sec-fetch-dest": "empty",
+            "sec-fetch-mode": "cors",
+            "sec-fetch-site": "none",
+            "sec-gpc": "1",
+            "user-agent": user_agent,
+            "x-secsdk-csrf-token": csrf_token,
+            "x-tt-params": tt_params,
+        }
+
+        self.logger.info(f"GET: %s\n\theaders: %s", url, headers)
+        r = requests.get(
+            url,
+            headers=headers,
+            cookies=self._get_cookies(**kwargs),
+            proxies=self._format_proxy(proxy),
+            **self.requests_extra_kwargs,
+        )
+
+        try:
+            json = r.json()
+            if (
+                json.get("type") == "verify"
+                or json.get("verifyConfig", {}).get("type", "") == "verify"
+            ):
+                self.logger.error(
+                    "Tiktok wants to display a captcha.\nResponse:\n%s\nCookies:\n%s",
+                    r.text,
+                    self._get_cookies(**kwargs),
+                )
+                raise TikTokCaptchaError()
+
+            # statusCode from props->pageProps->statusCode thanks @adiantek on #403
+            error_codes = {
+                "0": "OK",
+                "450": "CLIENT_PAGE_ERROR",
+                "10000": "VERIFY_CODE",
+                "10101": "SERVER_ERROR_NOT_500",
+                "10102": "USER_NOT_LOGIN",
+                "10111": "NET_ERROR",
+                "10113": "SHARK_SLIDE",
+                "10114": "SHARK_BLOCK",
+                "10119": "LIVE_NEED_LOGIN",
+                "10202": "USER_NOT_EXIST",
+                "10203": "MUSIC_NOT_EXIST",
+                "10204": "VIDEO_NOT_EXIST",
+                "10205": "HASHTAG_NOT_EXIST",
+                "10208": "EFFECT_NOT_EXIST",
+                "10209": "HASHTAG_BLACK_LIST",
+                "10210": "LIVE_NOT_EXIST",
+                "10211": "HASHTAG_SENSITIVITY_WORD",
+                "10212": "HASHTAG_UNSHELVE",
+                "10213": "VIDEO_LOW_AGE_M",
+                "10214": "VIDEO_LOW_AGE_T",
+                "10215": "VIDEO_ABNORMAL",
+                "10216": "VIDEO_PRIVATE_BY_USER",
+                "10217": "VIDEO_FIRST_REVIEW_UNSHELVE",
+                "10218": "MUSIC_UNSHELVE",
+                "10219": "MUSIC_NO_COPYRIGHT",
+                "10220": "VIDEO_UNSHELVE_BY_MUSIC",
+                "10221": "USER_BAN",
+                "10222": "USER_PRIVATE",
+                "10223": "USER_FTC",
+                "10224": "GAME_NOT_EXIST",
+                "10225": "USER_UNIQUE_SENSITIVITY",
+                "10227": "VIDEO_NEED_RECHECK",
+                "10228": "VIDEO_RISK",
+                "10229": "VIDEO_R_MASK",
+                "10230": "VIDEO_RISK_MASK",
+                "10231": "VIDEO_GEOFENCE_BLOCK",
+                "10404": "FYP_VIDEO_LIST_LIMIT",
+                "undefined": "MEDIA_ERROR",
+            }
+            statusCode = json.get("statusCode", 0)
+            self.logger.info(f"TikTok Returned: %s", json)
+            if statusCode == 10201:
+                # Invalid Entity
+                raise TikTokNotFoundError(
+                    "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")
+            elif statusCode != 0 and statusCode != -1:
+                raise GenericTikTokError(
+                    error_codes.get(
+                        statusCode, f"TikTok sent an unknown StatusCode of {statusCode}"
+                    )
+                )
+
+            return r.json()
+        except ValueError as e:
+            text = r.text
+            self.logger.info("TikTok response: %s", text)
+            if len(text) == 0:
+                raise EmptyResponseError(
+                    "Empty response from Tiktok to " + url
+                ) from None
+            else:
+                self.logger.exception("Converting response to JSON failed")
+                raise JSONDecodeFailure() from e
+
+ +
+ +

Makes requests to TikTok and returns their JSON.

+ +

This is all handled by the package so it's unlikely +you will need to use this.

+
+ + +
+
+
#   + + + def + external_signer(self, url, custom_device_id=None, verifyFp=None): +
+ +
+ View Source +
    def external_signer(self, url, custom_device_id=None, verifyFp=None):
+        """Makes requests to an external signer instead of using a browser.
+
+        ##### Parameters
+        * url: The server to make requests to
+            This server is designed to sign requests. You can find an example
+            of this signature server in the examples folder.
+
+        * custom_device_id: A TikTok parameter needed to download videos
+            The code generates these and handles these pretty well itself, however
+            for some things such as video download you will need to set a consistent
+            one of these.
+
+        * custom_verify_fp: A TikTok parameter needed to work most of the time,
+            To get this parameter look at [this video](https://youtu.be/zwLmLfVI-VQ?t=117)
+            I recommend watching the entire thing, as it will help setup this package.
+        """
+        if custom_device_id is not None:
+            query = {
+                "url": url,
+                "custom_device_id": custom_device_id,
+                "verifyFp": verifyFp,
+            }
+        else:
+            query = {"url": url, "verifyFp": verifyFp}
+        data = requests.get(
+            self.signer_url + "?{}".format(urlencode(query)),
+            **self.requests_extra_kwargs,
+        )
+        parsed_data = data.json()
+
+        return (
+            parsed_data["verifyFp"],
+            parsed_data["device_id"],
+            parsed_data["_signature"],
+            parsed_data["user_agent"],
+            parsed_data["referrer"],
+        )
+
+ +
+ +

Makes requests to an external signer instead of using a browser.

+ +
Parameters
+ +
    +
  • url: The server to make requests to +This server is designed to sign requests. You can find an example +of this signature server in the examples folder.

  • +
  • custom_device_id: A TikTok parameter needed to download videos +The code generates these and handles these pretty well itself, however +for some things such as video download you will need to set a consistent +one of these.

  • +
  • custom_verify_fp: A TikTok parameter needed to work most of the time, +To get this parameter look at this video +I recommend watching the entire thing, as it will help setup this package.

  • +
+
+ + +
+
+
#   + + + def + get_bytes(self, **kwargs) -> bytes: +
+ +
+ View Source +
    def get_bytes(self, **kwargs) -> bytes:
+        """Returns TikTok's response as bytes, similar to get_data"""
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+        if self.signer_url is None:
+            verify_fp, device_id, signature, _ = self.browser.sign_url(
+                calc_tt_params=False, **kwargs
+            )
+            user_agent = self.browser.user_agent
+            referrer = self.browser.referrer
+        else:
+            (
+                verify_fp,
+                device_id,
+                signature,
+                user_agent,
+                referrer,
+            ) = self.external_signer(
+                kwargs["url"], custom_device_id=kwargs.get("custom_device_id", None)
+            )
+        query = {"verifyFp": verify_fp, "_signature": signature}
+        url = "{}&{}".format(kwargs["url"], urlencode(query))
+        r = requests.get(
+            url,
+            headers={
+                "Accept": "*/*",
+                "Accept-Encoding": "identity;q=1, *;q=0",
+                "Accept-Language": "en-US;en;q=0.9",
+                "Cache-Control": "no-cache",
+                "Connection": "keep-alive",
+                "Host": url.split("/")[2],
+                "Pragma": "no-cache",
+                "Range": "bytes=0-",
+                "Referer": "https://www.tiktok.com/",
+                "User-Agent": user_agent,
+            },
+            proxies=self._format_proxy(proxy),
+            cookies=self._get_cookies(**kwargs),
+        )
+        return r.content
+
+ +
+ +

Returns TikTok's response as bytes, similar to get_data

+
+ + +
+
+
#   + +
@staticmethod
+ + def + generate_device_id(): +
+ +
+ View Source +
    @staticmethod
+    def generate_device_id():
+        """Generates a valid device_id for other methods. Pass this as the custom_device_id field to download videos"""
+        return "".join([random.choice(string.digits) for num in range(19)])
+
+ +
+ +

Generates a valid device_id for other methods. Pass this as the custom_device_id field to download videos

+
+ + +
+
+
+
+ #   + + + class + TikTokApi.user: +
+ +
+ View Source +
class User:
+    """
+    A TikTok User.
+
+    Example Usage
+    ```py
+    user = api.user(username='therock')
+    # or
+    user_id = '5831967'
+    sec_uid = 'MS4wLjABAAAA-VASjiXTh7wDDyXvjk10VFhMWUAoxr8bgfO1kAL1-9s'
+    user = api.user(user_id=user_id, sec_uid=sec_uid)
+    ```
+
+    """
+
+    parent: ClassVar[TikTokApi]
+
+    user_id: str
+    """The user ID of the user."""
+    sec_uid: str
+    """The sec UID of the user."""
+    username: str
+    """The username of the user."""
+    as_dict: dict
+    """The raw data associated with this user."""
+
+    def __init__(
+        self,
+        username: Optional[str] = None,
+        user_id: Optional[str] = None,
+        sec_uid: Optional[str] = None,
+        data: Optional[str] = None,
+    ):
+        """
+        You must provide the username or (user_id and sec_uid) otherwise this
+        will not function correctly.
+        """
+        self.__update_id_sec_uid_username(user_id, sec_uid, username)
+        if data is not None:
+            self.as_dict = data
+            self.__extract_from_data()
+
+    def info(self, **kwargs):
+        """
+        Returns a dictionary of TikTok's User object
+
+        Example Usage
+        ```py
+        user_data = api.user(username='therock').info()
+        ```
+        """
+        return self.info_full(**kwargs)["user"]
+
+    def info_full(self, **kwargs) -> dict:
+        """
+        Returns a dictionary of information associated with this User.
+        Includes statistics about this user.
+
+        Example Usage
+        ```py
+        user_data = api.user(username='therock').info_full()
+        ```
+        """
+
+        # TODO: Find the one using only user_id & sec_uid
+        if not self.username:
+            raise TypeError(
+                "You must provide the username when creating this class to use this method."
+            )
+
+        quoted_username = quote(self.username)
+        r = requests.get(
+            "https://tiktok.com/@{}?lang=en".format(quoted_username),
+            headers={
+                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
+                "path": "/@{}".format(quoted_username),
+                "Accept-Encoding": "gzip, deflate",
+                "Connection": "keep-alive",
+                "User-Agent": self.parent.user_agent,
+            },
+            proxies=User.parent._format_proxy(kwargs.get("proxy", None)),
+            cookies=User.parent._get_cookies(**kwargs),
+            **User.parent.requests_extra_kwargs,
+        )
+
+        data = extract_tag_contents(r.text)
+        user = json.loads(data)
+
+        user_props = user["props"]["pageProps"]
+        if user_props["statusCode"] == 404:
+            raise TikTokNotFoundError(
+                "TikTok user with username {} does not exist".format(self.username)
+            )
+
+        return user_props["userInfo"]
+
+    def videos(self, count=30, cursor=0, **kwargs) -> Generator[Video, None, None]:
+        """
+        Returns a Generator yielding Video objects.
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+            - cursor (int): The unix epoch to get videos since. TODO: Check this is right
+
+        Example Usage
+        ```py
+        user = api.user(username='therock')
+        for video in user.videos(count=100):
+            print(video.id)
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = User.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        if not self.user_id and not self.sec_uid:
+            self.__find_attributes()
+
+        first = True
+        amount_yielded = 0
+
+        while amount_yielded < count:
+            query = {
+                "count": 30,
+                "id": self.user_id,
+                "cursor": cursor,
+                "type": 1,
+                "secUid": self.sec_uid,
+                "sourceType": 8,
+                "appId": 1233,
+                "region": region,
+                "priority_region": region,
+                "language": language,
+            }
+            path = "api/post/item_list/?{}&{}".format(
+                User.parent._add_url_params(), urlencode(query)
+            )
+
+            res = User.parent.get_data(path, send_tt_params=True, **kwargs)
+
+            videos = res.get("itemList", [])
+            amount_yielded += len(videos)
+            for video in videos:
+                yield self.parent.video(data=video)
+
+            if not res.get("hasMore", False) and not first:
+                User.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            cursor = res["cursor"]
+            first = False
+
+    def liked(
+        self, count: int = 30, cursor: int = 0, **kwargs
+    ) -> Generator[Video, None, None]:
+        """
+        Returns a dictionary listing TikToks that a given a user has liked.
+
+        **Note**: The user's likes must be **public** (which is not the default option)
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+            - cursor (int): The unix epoch to get videos since. TODO: Check this is right
+
+        Example Usage
+        ```py
+        for liked_video in api.user(username='public_likes'):
+            print(liked_video.id)
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        amount_yielded = 0
+        first = True
+
+        if self.user_id is None and self.sec_uid is None:
+            self.__find_attributes()
+
+        while amount_yielded < count:
+            query = {
+                "count": 30,
+                "id": self.user_id,
+                "type": 2,
+                "secUid": self.sec_uid,
+                "cursor": cursor,
+                "sourceType": 9,
+                "appId": 1233,
+                "region": region,
+                "priority_region": region,
+                "language": language,
+            }
+            path = "api/favorite/item_list/?{}&{}".format(
+                User.parent._add_url_params(), urlencode(query)
+            )
+
+            res = self.parent.get_data(path, **kwargs)
+
+            if "itemList" not in res.keys():
+                User.parent.logger.error("User's likes are most likely private")
+                return
+
+            videos = res.get("itemList", [])
+            amount_yielded += len(videos)
+            for video in videos:
+                amount_yielded += 1
+                yield self.parent.video(data=video)
+
+            if not res.get("hasMore", False) and not first:
+                User.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            cursor = res["cursor"]
+            first = False
+
+    def __extract_from_data(self):
+        data = self.as_dict
+        keys = data.keys()
+
+        if "user_info" in keys:
+            self.__update_id_sec_uid_username(
+                data["user_info"]["uid"],
+                data["user_info"]["sec_uid"],
+                data["user_info"]["unique_id"],
+            )
+        elif "uniqueId" in keys:
+            self.__update_id_sec_uid_username(
+                data["id"], data["secUid"], data["uniqueId"]
+            )
+
+        if None in (self.username, self.user_id, self.sec_uid):
+            User.parent.logger.error(
+                f"Failed to create User with data: {data}\nwhich has keys {data.keys()}"
+            )
+
+    def __update_id_sec_uid_username(self, id, sec_uid, username):
+        self.user_id = id
+        self.sec_uid = sec_uid
+        self.username = username
+
+    def __find_attributes(self) -> None:
+        # It is more efficient to check search first, since self.user_object() makes HTML request.
+        found = False
+        for u in self.parent.search.users(self.username):
+            if u.username == self.username:
+                found = True
+                self.__update_id_sec_uid_username(u.user_id, u.sec_uid, u.username)
+                break
+
+        if not found:
+            user_object = self.info()
+            self.__update_id_sec_uid_username(
+                user_object["id"], user_object["secUid"], user_object["uniqueId"]
+            )
+
+    def __repr__(self):
+        return self.__str__()
+
+    def __str__(self):
+        return f"TikTokApi.user(username='{self.username}', user_id='{self.user_id}', sec_uid='{self.sec_uid}')"
+
+    def __getattr__(self, name):
+        if name in ["as_dict"]:
+            self.as_dict = self.info()
+            self.__extract_from_data()
+            return self.__getattribute__(name)
+
+        raise AttributeError(f"{name} doesn't exist on TikTokApi.api.User")
+
+ +
+ +

A TikTok User.

+ +

Example Usage

+ +
user = api.user(username='therock')
+# or
+user_id = '5831967'
+sec_uid = 'MS4wLjABAAAA-VASjiXTh7wDDyXvjk10VFhMWUAoxr8bgfO1kAL1-9s'
+user = api.user(user_id=user_id, sec_uid=sec_uid)
+
+
+ + +
+
Inherited Members
+
+ +
+
+
+
+
+ #   + + + class + TikTokApi.search: +
+ +
+ View Source +
class Search:
+    """Contains static methods about searching."""
+
+    parent: TikTokApi
+
+    @staticmethod
+    def users(search_term, count=28, **kwargs) -> Generator[User, None, None]:
+        """
+        Searches for users.
+
+        - Parameters:
+            - search_term (str): The phrase you want to search for.
+            - count (int): The amount of videos you want returned.
+
+        Example Usage
+        ```py
+        for user in api.search.users('therock'):
+            # do something
+        ```
+        """
+        return Search.discover_type(search_term, prefix="user", count=count, **kwargs)
+
+    @staticmethod
+    def sounds(search_term, count=28, **kwargs) -> Generator[Sound, None, None]:
+        """
+        Searches for sounds.
+
+        - Parameters:
+            - search_term (str): The phrase you want to search for.
+            - count (int): The amount of videos you want returned.
+
+        Example Usage
+        ```py
+        for user in api.search.sounds('funny'):
+            # do something
+        ```
+        """
+        return Search.discover_type(search_term, prefix="music", count=count, **kwargs)
+
+    @staticmethod
+    def hashtags(search_term, count=28, **kwargs) -> Generator[Hashtag, None, None]:
+        """
+        Searches for hashtags/challenges.
+
+        - Parameters:
+            - search_term (str): The phrase you want to search for.
+            - count (int): The amount of videos you want returned.
+
+        Example Usage
+        ```py
+        for user in api.search.hashtags('funny'):
+            # do something
+        ```
+        """
+        return Search.discover_type(
+            search_term, prefix="challenge", count=count, **kwargs
+        )
+
+    @staticmethod
+    def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> list:
+        """
+        Searches for a specific type of object.
+        You should instead use the users/sounds/hashtags as they all use data
+        from this function.
+
+        - Parameters:
+            - search_term (str): The phrase you want to search for.
+            - prefix (str): either user|music|challenge
+            - count (int): The amount of videos you want returned.
+
+        Example Usage
+        ```py
+        for user in api.search.discover_type('therock', 'user'):
+            # do something
+        ```
+
+        """
+        # TODO: Investigate if this is actually working as expected. Doesn't seem to be
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = Search.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        cursor = offset
+        page_size = 28
+
+        while cursor - offset < count:
+            query = {
+                "discoverType": 0,
+                "needItemList": False,
+                "keyWord": search_term,
+                "offset": cursor,
+                "count": page_size,
+                "useRecommend": False,
+                "language": "en",
+            }
+            path = "api/discover/{}/?{}&{}".format(
+                prefix, Search.parent._add_url_params(), urlencode(query)
+            )
+            data = Search.parent.get_data(path, **kwargs)
+
+            for x in data.get("userInfoList", []):
+                yield User(data=x["user"])
+
+            for x in data.get("musicInfoList", []):
+                yield Sound(data=x["music"])
+
+            for x in data.get("challengeInfoList", []):
+                yield Hashtag(data=x["challenge"])
+
+            if int(data["offset"]) <= offset:
+                Search.parent.logger.info(
+                    "TikTok is not sending videos beyond this point."
+                )
+                return
+
+            offset = int(data["offset"])
+
+    @staticmethod
+    def users_alternate(search_term, count=28, offset=0, **kwargs) -> list:
+        """
+        Searches for users using an alternate endpoint than Search.users
+
+        - Parameters:
+            - search_term (str): The phrase you want to search for.
+            - count (int): The amount of videos you want returned.
+
+        Example Usage
+        ```py
+        for user in api.search.users_alternate('therock'):
+            # do something
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = Search.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        cursor = offset
+
+        spawn = requests.head(
+            "https://www.tiktok.com",
+            proxies=Search.parent._format_proxy(proxy),
+            **Search.parent.requests_extra_kwargs
+        )
+        ttwid = spawn.cookies["ttwid"]
+
+        # For some reason when <= it can be off by one.
+        while cursor - offset <= count:
+            query = {
+                "keyword": search_term,
+                "cursor": cursor,
+                "app_language": "en",
+            }
+            path = "api/search/{}/full/?{}&{}".format(
+                "user", Search.parent._add_url_params(), urlencode(query)
+            )
+
+            data = Search.parent.get_data(
+                path, use_desktop_base_url=True, ttwid=ttwid, **kwargs
+            )
+
+            # When I move to 3.10+ support make this a match switch.
+            for result in data.get("user_list", []):
+                yield User(data=result)
+
+            if data.get("has_more", 0) == 0:
+                Search.parent.logger.info(
+                    "TikTok is not sending videos beyond this point."
+                )
+                return
+
+            cursor = int(data.get("cursor"))
+
+ +
+ +

Contains static methods about searching.

+
+ + + +
+
+
+ #   + + + class + TikTokApi.sound: +
+ +
+ View Source +
class Sound:
+    """
+    A TikTok Sound/Music/Song.
+
+    Example Usage
+    ```py
+    song = api.song(id='7016547803243022337')
+    ```
+    """
+
+    parent: ClassVar[TikTokApi]
+
+    id: str
+    """TikTok's ID for the sound"""
+    title: Optional[str]
+    """The title of the song."""
+    author: Optional[User]
+    """The author of the song (if it exists)"""
+
+    def __init__(self, id: Optional[str] = None, data: Optional[str] = None):
+        """
+        You must provide the id of the sound or it will not work.
+        """
+        if data is not None:
+            self.as_dict = data
+            self.__extract_from_data()
+        elif id is None:
+            raise TypeError("You must provide id parameter.")
+        else:
+            self.id = id
+
+    def info(self, use_html=False, **kwargs) -> dict:
+        """
+        Returns a dictionary of TikTok's Sound/Music object.
+
+        - Parameters:
+            - use_html (bool): If you want to perform an HTML request or not.
+                Defaults to False to use an API call, which shouldn't get detected
+                as often as an HTML request.
+
+
+        Example Usage
+        ```py
+        sound_data = api.sound(id='7016547803243022337').info()
+        ```
+        """
+        if use_html:
+            return self.info_full(**kwargs)["musicInfo"]
+
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        path = "node/share/music/-{}?{}".format(self.id, self.parent._add_url_params())
+        res = self.parent.get_data(path, **kwargs)
+
+        if res.get("statusCode", 200) == 10203:
+            raise TikTokNotFoundError()
+
+        return res["musicInfo"]["music"]
+
+    def info_full(self, **kwargs) -> dict:
+        """
+        Returns all the data associated with a TikTok Sound.
+
+        This makes an API request, there is no HTML request option, as such
+        with Sound.info()
+
+        Example Usage
+        ```py
+        sound_data = api.sound(id='7016547803243022337').info_full()
+        ```
+        """
+        r = requests.get(
+            "https://www.tiktok.com/music/-{}".format(self.id),
+            headers={
+                "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
+                "Accept-Encoding": "gzip, deflate",
+                "Connection": "keep-alive",
+                "User-Agent": self.parent.user_agent,
+            },
+            proxies=self.parent._format_proxy(kwargs.get("proxy", None)),
+            cookies=self.parent._get_cookies(**kwargs),
+            **self.parent.requests_extra_kwargs,
+        )
+
+        data = extract_tag_contents(r.text)
+        return json.loads(data)["props"]["pageProps"]["musicInfo"]
+
+    def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]:
+        """
+        Returns Video objects of videos created with this sound.
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+            - cursor (int): The unix epoch to get videos since. TODO: Check this is right
+
+        Example Usage
+        ```py
+        for video in api.sound(id='7016547803243022337').videos():
+            # do something
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        cursor = offset
+        page_size = 30
+
+        while cursor - offset < count:
+            query = {
+                "secUid": "",
+                "musicID": self.id,
+                "cursor": cursor,
+                "shareUid": "",
+                "count": page_size,
+            }
+            path = "api/music/item_list/?{}&{}".format(
+                self.parent._add_url_params(), urlencode(query)
+            )
+
+            res = self.parent.get_data(path, send_tt_params=True, **kwargs)
+
+            for result in res.get("itemList", []):
+                yield self.parent.video(data=result)
+
+            if not res.get("hasMore", False):
+                self.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            cursor = int(res["cursor"])
+
+    def __extract_from_data(self):
+        data = self.as_dict
+        keys = data.keys()
+
+        if "authorName" in keys:
+            self.id = data["id"]
+            self.title = data["title"]
+
+            if data.get("authorName") is not None:
+                self.author = self.parent.user(username=data["authorName"])
+
+        if self.id is None:
+            Sound.parent.logger.error(
+                f"Failed to create Sound with data: {data}\nwhich has keys {data.keys()}"
+            )
+
+    def __repr__(self):
+        return self.__str__()
+
+    def __str__(self):
+        return f"TikTokApi.sound(id='{self.id}')"
+
+    def __getattr__(self, name):
+        if name in ["title", "author", "as_dict"]:
+            self.as_dict = self.info()
+            self.__extract_from_data()
+            return self.__getattribute__(name)
+
+        raise AttributeError(f"{name} doesn't exist on TikTokApi.api.Sound")
+
+ +
+ +

A TikTok Sound/Music/Song.

+ +

Example Usage

+ +
song = api.song(id='7016547803243022337')
+
+
+ + +
+
Inherited Members
+
+ +
+
+
+
+
+ #   + + + class + TikTokApi.hashtag: +
+ +
+ View Source +
class Hashtag:
+    """
+    A TikTok Hashtag/Challenge.
+
+    Example Usage
+    ```py
+    hashtag = api.hashtag(name='funny')
+    ```
+    """
+
+    parent: ClassVar[TikTokApi]
+
+    id: str
+    """The ID of the hashtag"""
+    name: str
+    """The name of the hashtag (omiting the #)"""
+    as_dict: dict
+    """The raw data associated with this hashtag."""
+
+    def __init__(
+        self,
+        name: Optional[str] = None,
+        id: Optional[str] = None,
+        data: Optional[str] = None,
+    ):
+        """
+        You must provide the name or id of the hashtag.
+        """
+        self.name = name
+        self.id = id
+
+        if data is not None:
+            self.as_dict = data
+            self.__extract_from_data()
+
+    def info(self, **kwargs) -> dict:
+        """
+        Returns TikTok's dictionary representation of the hashtag object.
+        """
+        return self.info_full(**kwargs)["challengeInfo"]["challenge"]
+
+    def info_full(self, **kwargs) -> dict:
+        """
+        Returns all information sent by TikTok related to this hashtag.
+
+        Example Usage
+        ```py
+        hashtag_data = api.hashtag(name='funny').info_full()
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        if self.name is not None:
+            query = {"challengeName": self.name}
+        else:
+            query = {"challengeId": self.id}
+        path = "api/challenge/detail/?{}&{}".format(
+            self.parent._add_url_params(), urlencode(query)
+        )
+
+        data = self.parent.get_data(path, **kwargs)
+
+        if data["challengeInfo"].get("challenge") is None:
+            raise TikTokNotFoundError("Challenge {} does not exist".format(self.name))
+
+        return data
+
+    def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]:
+        """Returns a dictionary listing TikToks with a specific hashtag.
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+            - cursor (int): The unix epoch to get videos since. TODO: Check this is right
+
+        Example Usage
+        ```py
+        for video in api.hashtag(name='funny').videos():
+            # do something
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        if self.id is None:
+            self.id = self.info()["id"]
+
+        cursor = offset
+        page_size = 30
+
+        while cursor - offset < count:
+            query = {
+                "count": page_size,
+                "challengeID": self.id,
+                "cursor": cursor,
+            }
+            path = "api/challenge/item_list/?{}&{}".format(
+                self.parent._add_url_params(), urlencode(query)
+            )
+            res = self.parent.get_data(path, **kwargs)
+
+            for result in res.get("itemList", []):
+                yield self.parent.video(data=result)
+
+            if not res.get("hasMore", False):
+                self.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            cursor = int(res["cursor"])
+
+    def __extract_from_data(self):
+        data = self.as_dict
+        keys = data.keys()
+
+        if "title" in keys:
+            self.id = data["id"]
+            self.name = data["title"]
+
+        if None in (self.name, self.id):
+            Hashtag.parent.logger.error(
+                f"Failed to create Hashtag with data: {data}\nwhich has keys {data.keys()}"
+            )
+
+    def __repr__(self):
+        return self.__str__()
+
+    def __str__(self):
+        return f"TikTokApi.hashtag(id='{self.id}', name='{self.name}')"
+
+    def __getattr__(self, name):
+        if name in ["id", "name", "as_dict"]:
+            self.as_dict = self.info()
+            self.__extract_from_data()
+            return self.__getattribute__(name)
+
+        raise AttributeError(f"{name} doesn't exist on TikTokApi.api.Hashtag")
+
+ +
+ +

A TikTok Hashtag/Challenge.

+ +

Example Usage

+ +
hashtag = api.hashtag(name='funny')
+
+
+ + +
+
Inherited Members
+
+ +
+
+
+
+
+ #   + + + class + TikTokApi.video: +
+ +
+ View Source +
class Video:
+    """
+    A TikTok Video class
+
+    Example Usage
+    ```py
+    video = api.video(id='7041997751718137094')
+    ```
+    """
+
+    parent: ClassVar[TikTokApi]
+
+    id: str
+    """TikTok's ID of the Video"""
+    author: Optional[User]
+    """The User who created the Video"""
+    sound: Optional[Sound]
+    """The Sound that is associated with the Video"""
+    hashtags: Optional[list[Hashtag]]
+    """A List of Hashtags on the Video"""
+    as_dict: dict
+    """The raw data associated with this Video."""
+
+    def __init__(
+        self,
+        id: Optional[str] = None,
+        url: Optional[str] = None,
+        data: Optional[dict] = None,
+    ):
+        """
+        You must provide the id or a valid url, else this will fail.
+        """
+        self.id = id
+        if data is not None:
+            self.as_dict = data
+            self.__extract_from_data()
+        elif url is not None:
+            self.id = extract_video_id_from_url(url)
+
+        if self.id is None:
+            raise TypeError("You must provide id or url parameter.")
+
+    def info(self, **kwargs) -> dict:
+        """
+        Returns a dictionary of TikTok's Video object.
+
+        Example Usage
+        ```py
+        video_data = api.video(id='7041997751718137094').info()
+        ```
+        """
+        return self.info_full(**kwargs)["itemInfo"]["itemStruct"]
+
+    def info_full(self, **kwargs) -> dict:
+        """
+        Returns a dictionary of all data associated with a TikTok Video.
+
+        Example Usage
+        ```py
+        video_data = api.video(id='7041997751718137094').info_full()
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        device_id = kwargs.get("custom_device_id", None)
+        query = {
+            "itemId": self.id,
+        }
+        path = "api/item/detail/?{}&{}".format(
+            self.parent._add_url_params(), urlencode(query)
+        )
+
+        return self.parent.get_data(path, **kwargs)
+
+    def bytes(self, **kwargs) -> bytes:
+        """
+        Returns the bytes of a TikTok Video.
+
+        Example Usage
+        ```py
+        video_bytes = api.video(id='7041997751718137094').bytes()
+
+        # Saving The Video
+        with open('saved_video.mp4', 'wb') as output:
+            output.write(video_bytes)
+        ```
+        """
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = self.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        video_data = self.info(**kwargs)
+        download_url = video_data["video"]["playAddr"]
+
+        return self.parent.get_bytes(url=download_url, **kwargs)
+
+    def __extract_from_data(self) -> None:
+        data = self.as_dict
+        keys = data.keys()
+
+        if "author" in keys:
+            self.id = data["id"]
+            self.author = self.parent.user(data=data["author"])
+            self.sound = self.parent.sound(data=data["music"])
+
+            self.hashtags = [
+                self.parent.hashtag(data=hashtag)
+                for hashtag in data.get("challenges", [])
+            ]
+
+        if self.id is None:
+            Video.parent.logger.error(
+                f"Failed to create Video with data: {data}\nwhich has keys {data.keys()}"
+            )
+
+    def __repr__(self):
+        return self.__str__()
+
+    def __str__(self):
+        return f"TikTokApi.video(id='{self.id}')"
+
+    def __getattr__(self, name):
+        # Handle author, sound, hashtags, as_dict
+        if name in ["author", "sound", "hashtags", "as_dict"]:
+            self.as_dict = self.info()
+            self.__extract_from_data()
+            return self.__getattribute__(name)
+
+        raise AttributeError(f"{name} doesn't exist on TikTokApi.api.Video")
+
+ +
+ +

A TikTok Video class

+ +

Example Usage

+ +
video = api.video(id='7041997751718137094')
+
+
+ + +
+
Inherited Members
+
+ +
+
+
+
+
+ #   + + + class + TikTokApi.trending: +
+ +
+ View Source +
class Trending:
+    """Contains static methods related to trending."""
+
+    parent: TikTokApi
+
+    @staticmethod
+    def videos(count=30, **kwargs) -> Generator[Video, None, None]:
+        """
+        Returns Videos that are trending on TikTok.
+
+        - Parameters:
+            - count (int): The amount of videos you want returned.
+        """
+
+        (
+            region,
+            language,
+            proxy,
+            maxCount,
+            device_id,
+        ) = Trending.parent._process_kwargs(kwargs)
+        kwargs["custom_device_id"] = device_id
+
+        spawn = requests.head(
+            "https://www.tiktok.com",
+            proxies=Trending.parent._format_proxy(proxy),
+            **Trending.parent.requests_extra_kwargs,
+        )
+        ttwid = spawn.cookies["ttwid"]
+
+        first = True
+        amount_yielded = 0
+
+        while amount_yielded < count:
+            query = {
+                "count": 30,
+                "id": 1,
+                "sourceType": 12,
+                "itemID": 1,
+                "insertedItemID": "",
+                "region": region,
+                "priority_region": region,
+                "language": language,
+            }
+            path = "api/recommend/item_list/?{}&{}".format(
+                Trending.parent._add_url_params(), urlencode(query)
+            )
+            res = Trending.parent.get_data(path, ttwid=ttwid, **kwargs)
+            for result in res.get("itemList", []):
+                yield Video(data=result)
+            amount_yielded += len(res.get("itemList", []))
+
+            if not res.get("hasMore", False) and not first:
+                Trending.parent.logger.info(
+                    "TikTok isn't sending more TikToks beyond this point."
+                )
+                return
+
+            first = False
+
+ +
+ +

Contains static methods related to trending.

+
+ + +
+
Inherited Members
+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/docs/TikTokApi/utilities.html b/docs/TikTokApi/utilities.html new file mode 100644 index 00000000..d086ba64 --- /dev/null +++ b/docs/TikTokApi/utilities.html @@ -0,0 +1,398 @@ + + + + + + + TikTokApi.utilities API documentation + + + + + + + + +
+
+

+TikTokApi.utilities

+ + +
+ View Source +
import subprocess
+import sys
+
+LOGGER_NAME: str = "TikTokApi"
+
+
+def update_messager():
+    if not check("TikTokApi"):
+        # Outdated
+        print(
+            "TikTokApi package is outdated, please consider upgrading! \n(You can suppress this by setting ignore_version=True in the TikTokApi constructor)"
+        )
+
+    if not check_future_deprecation():
+        print(
+            "Your version of python is going to be deprecated, for future updates upgrade to 3.7+"
+        )
+
+
+def check(name):
+
+    latest_version = str(
+        subprocess.run(
+            [sys.executable, "-m", "pip", "install", "{}==random".format(name)],
+            capture_output=True,
+            text=True,
+        )
+    )
+    latest_version = latest_version[latest_version.find("(from versions:") + 15 :]
+    latest_version = latest_version[: latest_version.find(")")]
+    latest_version = latest_version.replace(" ", "").split(",")[-1]
+
+    current_version = str(
+        subprocess.run(
+            [sys.executable, "-m", "pip", "show", "{}".format(name)],
+            capture_output=True,
+            text=True,
+        )
+    )
+    current_version = current_version[current_version.find("Version:") + 8 :]
+    current_version = current_version[: current_version.find("\\n")].replace(" ", "")
+
+    if latest_version == current_version:
+        return True
+    else:
+        return False
+
+
+def check_future_deprecation():
+    return sys.version_info >= (3, 7)
+
+ +
+ +
+
+
#   + + LOGGER_NAME: str = 'TikTokApi' +
+ + + +
+
+
#   + + + def + update_messager(): +
+ +
+ View Source +
def update_messager():
+    if not check("TikTokApi"):
+        # Outdated
+        print(
+            "TikTokApi package is outdated, please consider upgrading! \n(You can suppress this by setting ignore_version=True in the TikTokApi constructor)"
+        )
+
+    if not check_future_deprecation():
+        print(
+            "Your version of python is going to be deprecated, for future updates upgrade to 3.7+"
+        )
+
+ +
+ + + +
+
+
#   + + + def + check(name): +
+ +
+ View Source +
def check(name):
+
+    latest_version = str(
+        subprocess.run(
+            [sys.executable, "-m", "pip", "install", "{}==random".format(name)],
+            capture_output=True,
+            text=True,
+        )
+    )
+    latest_version = latest_version[latest_version.find("(from versions:") + 15 :]
+    latest_version = latest_version[: latest_version.find(")")]
+    latest_version = latest_version.replace(" ", "").split(",")[-1]
+
+    current_version = str(
+        subprocess.run(
+            [sys.executable, "-m", "pip", "show", "{}".format(name)],
+            capture_output=True,
+            text=True,
+        )
+    )
+    current_version = current_version[current_version.find("Version:") + 8 :]
+    current_version = current_version[: current_version.find("\\n")].replace(" ", "")
+
+    if latest_version == current_version:
+        return True
+    else:
+        return False
+
+ +
+ + + +
+
+
#   + + + def + check_future_deprecation(): +
+ +
+ View Source +
def check_future_deprecation():
+    return sys.version_info >= (3, 7)
+
+ +
+ + + +
+
+ + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 00000000..13720cff --- /dev/null +++ b/docs/index.html @@ -0,0 +1,7 @@ + + + + + + + diff --git a/docs/search.js b/docs/search.js new file mode 100644 index 00000000..3cc7ffe9 --- /dev/null +++ b/docs/search.js @@ -0,0 +1,46 @@ +window.pdocSearch = (function(){ +/** elasticlunr - http://weixsong.github.io * Copyright (C) 2017 Oliver Nightingale * Copyright (C) 2017 Wei Song * MIT Licensed */!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();oUnofficial TikTok API in Python

\n\n

This is an unofficial api wrapper for TikTok.com in python. With this api you are able to call most trending and fetch specific user information as well as much more.

\n\n

\"DOI\" \"LinkedIn\" \"Sponsor \"GitHub \"Build \"GitHub\" \"Downloads\" \"\" \"Support

\n\n

Sponsors

\n\n

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.

\n\n

\"TikAPI\" | TikAPI is a paid TikTok API service providing an full out-of-the-box solution for developers, trusted by 100+ companies. Learn more\n:-------------------------:|:-------------------------:

\n\n

Table of Contents

\n\n\n\n

Upgrading from V4 to V5

\n\n

Documentation

\n\n

You can find the full documentation here, the TikTokApi Class is where you'll probably spend most of your time.

\n\n

Getting Started

\n\n

To get started using this api follow the instructions below.

\n\n

How to Support The Project

\n\n
    \n
  • Star the repo \ud83d\ude0e
  • \n
  • Consider sponsoring me on GitHub
  • \n
  • Send me an email or a LinkedIn message telling me what you're using the API for, I really like hearing what people are using it for.
  • \n
  • Submit PRs for issues :)
  • \n
\n\n

Installing

\n\n

If you run into an issue please check the closed issues on the github, although feel free to re-open a new issue if you find an issue that's been closed for a few months. The codebase can and does run into similar issues as it has before, because TikTok changes things up.

\n\n
pip install TikTokApi\npython -m playwright install\n
\n\n

If you would prefer a video walk through of setting up this package I created a currently semi-outdated (TODO: new one for v5 coming soon) YouTube video just for that.

\n\n

Docker Installation

\n\n

Clone this repository onto a local machine (or just the Dockerfile since it installs TikTokApi from pip) then run the following commands.

\n\n
docker pull mcr.microsoft.com/playwright:focal\ndocker build . -t tiktokapi:latest\ndocker run -v TikTokApi --rm tiktokapi:latest python3 your_script.py\n
\n\n

Note this assumes your script is named your_script.py and lives in the root of this directory.

\n\n

Common Issues

\n\n

Please don't open an issue if you're experiencing one of these just comment if the provided solution do not work for you.

\n\n
    \n
  • Browser Has no Attribute - make sure you ran python3 -m playwright install, if your error persists try the playwright-python quickstart guide and diagnose issues from there.
  • \n
\n\n

Quick Start Guide

\n\n

Here's a quick bit of code to get the most recent trending videos on TikTok. There's more examples in the examples directory.

\n\n
from TikTokApi import TikTokApi\n\n# In your web browser you will need to go to TikTok, check the cookies \n# and under www.tiktok.com s_v_web_id should exist, and use that value\n# as input to custom_verify_fp\n# Or watch https://www.youtube.com/watch?v=zwLmLfVI-VQ for a visual\n# TODO: Update link\napi = TikTokApi(custom_verify_fp="")\n\nfor trending_video in api.trending.videos(count=50):\n    # Prints the author's username of the trending video.\n    print(trending_video.author.username)\n
\n\n

To run the example scripts from the repository root, make sure you use the -m option on python.

\n\n
python -m examples.get_trending\n
\n\n

You can access the dictionary type of an object using .as_dict. On a video this may look like\nthis, although TikTok changes their structure from time to time so it's worth investigating the structure of the dictionary when you use this package.

\n\n

Upgrading from V4 to V5

\n\n

All changes will be noted on #803 if you want more information.

\n\n

Motivation

\n\n

This package has been difficult to maintain due to it's structure, difficult to work with since the user of the package must write parsing methods to extract information from dictionaries, more memory intensive than it needs to be (although this can be further improved), and in general just difficult to work with for new users.

\n\n

As a result, I've decided to at least attempt to remedy some of these issues, the biggest changes are that

\n\n
    \n
  1. The package has shifted to using classes for different TikTok objects resulting in an easier, higher-level programming experience.
  2. \n
  3. All methods that used to return a list of objects have been switched to using generators, to hopefully decrease memory utilization for most users.
  4. \n
\n\n

Upgrading Examples

\n\n

Accessing Dictionary on Objects (similar to V4)

\n\n

TODO: Make video upgrading from V4-V5?

\n\n

You'll probably need to use this beyond just for legacy support, since not all attributes are parsed out and attached\nto the different objects.

\n\n

You may want to use this as a workaround for legacy applications while you upgrade the rest of the app. I'd suggest that you do eventually upgrade to using the higher-level approach fully.

\n\n
user = api.user(username='therock')\nuser.as_dict # -> dict of the user_object\nfor video in user.videos():\n    video.as_dict # -> dict of TikTok's video object as found when requesting the videos endpoint\n
\n\n

Here's a few more examples that help illustrate the differences in the flow of the usage of the package with V5.

\n\n
# V4\napi = TikTokApi.get_instance()\ntrending_videos = api.by_trending()\n\n#V5\napi = TikTokApi() # .get_instance no longer exists\nfor trending_video in api.trending.videos():\n    # do something\n
\n\n

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.

\n\n
# V4\ntrending_videos = api.by_trending()\nfor video in trending_videos:\n    # The dictionary responses are also different depending on what endpoint you got them from\n    # So, it's usually more painful than this to deal with\n    trending_user = api.get_user(id=video['author']['id'], secUid=video['author']['secUid'])\n\n\n# V5\n# This is more complicated than above, but it illustrates the simplified approach\nfor trending_video in api.trending.videos():\n    user_stats = trending_video.author.info_full['stats']\n    if user_stats['followerCount'] >= 10000:\n        # maybe save the user in a database\n
\n"}, {"fullname": "TikTokApi.api", "modulename": "TikTokApi.api", "type": "module", "doc": "

This module contains classes that all represent different types of data sent back by the TikTok servers.

\n\n

The files within in module correspond to what type of object is described and all have different methods associated with them.

\n"}, {"fullname": "TikTokApi.api.hashtag", "modulename": "TikTokApi.api.hashtag", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.hashtag.Hashtag", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag", "type": "class", "doc": "

A TikTok Hashtag/Challenge.

\n\n

Example Usage

\n\n
hashtag = api.hashtag(name='funny')\n
\n"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.__init__", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.__init__", "type": "function", "doc": "

You must provide the name or id of the hashtag.

\n", "signature": "(\n self,\n name: Optional[str] = None,\n id: Optional[str] = None,\n data: Optional[str] = None\n)", "funcdef": "def"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.id", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.id", "type": "variable", "doc": "

The ID of the hashtag

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.name", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.name", "type": "variable", "doc": "

The name of the hashtag (omiting the #)

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.as_dict", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.as_dict", "type": "variable", "doc": "

The raw data associated with this hashtag.

\n", "annotation": ": dict"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.info", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.info", "type": "function", "doc": "

Returns TikTok's dictionary representation of the hashtag object.

\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.info_full", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.info_full", "type": "function", "doc": "

Returns all information sent by TikTok related to this hashtag.

\n\n

Example Usage

\n\n
hashtag_data = api.hashtag(name='funny').info_full()\n
\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.videos", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.videos", "type": "function", "doc": "

Returns a dictionary listing TikToks with a specific hashtag.

\n\n
    \n
  • Parameters:\n
      \n
    • count (int): The amount of videos you want returned.
    • \n
    • cursor (int): The unix epoch to get videos since. TODO: Check this is right
    • \n
  • \n
\n\n

Example Usage

\n\n
for video in api.hashtag(name='funny').videos():\n    # do something\n
\n", "signature": "(\n self,\n count=30,\n offset=0,\n **kwargs\n) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]", "funcdef": "def"}, {"fullname": "TikTokApi.api.search", "modulename": "TikTokApi.api.search", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.search.Search", "modulename": "TikTokApi.api.search", "qualname": "Search", "type": "class", "doc": "

Contains static methods about searching.

\n"}, {"fullname": "TikTokApi.api.search.Search.__init__", "modulename": "TikTokApi.api.search", "qualname": "Search.__init__", "type": "function", "doc": "

\n", "signature": "()", "funcdef": "def"}, {"fullname": "TikTokApi.api.search.Search.users", "modulename": "TikTokApi.api.search", "qualname": "Search.users", "type": "function", "doc": "

Searches for users.

\n\n
    \n
  • Parameters:\n
      \n
    • search_term (str): The phrase you want to search for.
    • \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n\n

Example Usage

\n\n
for user in api.search.users('therock'):\n    # do something\n
\n", "signature": "(\n search_term,\n count=28,\n **kwargs\n) -> Generator[TikTokApi.api.user.User, NoneType, NoneType]", "funcdef": "def"}, {"fullname": "TikTokApi.api.search.Search.sounds", "modulename": "TikTokApi.api.search", "qualname": "Search.sounds", "type": "function", "doc": "

Searches for sounds.

\n\n
    \n
  • Parameters:\n
      \n
    • search_term (str): The phrase you want to search for.
    • \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n\n

Example Usage

\n\n
for user in api.search.sounds('funny'):\n    # do something\n
\n", "signature": "(\n search_term,\n count=28,\n **kwargs\n) -> Generator[TikTokApi.api.sound.Sound, NoneType, NoneType]", "funcdef": "def"}, {"fullname": "TikTokApi.api.search.Search.hashtags", "modulename": "TikTokApi.api.search", "qualname": "Search.hashtags", "type": "function", "doc": "

Searches for hashtags/challenges.

\n\n
    \n
  • Parameters:\n
      \n
    • search_term (str): The phrase you want to search for.
    • \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n\n

Example Usage

\n\n
for user in api.search.hashtags('funny'):\n    # do something\n
\n", "signature": "(\n search_term,\n count=28,\n **kwargs\n) -> Generator[TikTokApi.api.hashtag.Hashtag, NoneType, NoneType]", "funcdef": "def"}, {"fullname": "TikTokApi.api.search.Search.discover_type", "modulename": "TikTokApi.api.search", "qualname": "Search.discover_type", "type": "function", "doc": "

Searches for a specific type of object.\nYou should instead use the users/sounds/hashtags as they all use data\nfrom this function.

\n\n
    \n
  • Parameters:\n
      \n
    • search_term (str): The phrase you want to search for.
    • \n
    • prefix (str): either user|music|challenge
    • \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n\n

Example Usage

\n\n
for user in api.search.discover_type('therock', 'user'):\n    # do something\n
\n", "signature": "(search_term, prefix, count=28, offset=0, **kwargs) -> list", "funcdef": "def"}, {"fullname": "TikTokApi.api.search.Search.users_alternate", "modulename": "TikTokApi.api.search", "qualname": "Search.users_alternate", "type": "function", "doc": "

Searches for users using an alternate endpoint than Search.users

\n\n
    \n
  • Parameters:\n
      \n
    • search_term (str): The phrase you want to search for.
    • \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n\n

Example Usage

\n\n
for user in api.search.users_alternate('therock'):\n    # do something\n
\n", "signature": "(search_term, count=28, offset=0, **kwargs) -> list", "funcdef": "def"}, {"fullname": "TikTokApi.api.sound", "modulename": "TikTokApi.api.sound", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.sound.Sound", "modulename": "TikTokApi.api.sound", "qualname": "Sound", "type": "class", "doc": "

A TikTok Sound/Music/Song.

\n\n

Example Usage

\n\n
song = api.song(id='7016547803243022337')\n
\n"}, {"fullname": "TikTokApi.api.sound.Sound.__init__", "modulename": "TikTokApi.api.sound", "qualname": "Sound.__init__", "type": "function", "doc": "

You must provide the id of the sound or it will not work.

\n", "signature": "(self, id: Optional[str] = None, data: Optional[str] = None)", "funcdef": "def"}, {"fullname": "TikTokApi.api.sound.Sound.id", "modulename": "TikTokApi.api.sound", "qualname": "Sound.id", "type": "variable", "doc": "

TikTok's ID for the sound

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.sound.Sound.title", "modulename": "TikTokApi.api.sound", "qualname": "Sound.title", "type": "variable", "doc": "

The title of the song.

\n", "annotation": ": Optional[str]"}, {"fullname": "TikTokApi.api.sound.Sound.author", "modulename": "TikTokApi.api.sound", "qualname": "Sound.author", "type": "variable", "doc": "

The author of the song (if it exists)

\n", "annotation": ": Optional[TikTokApi.api.user.User]"}, {"fullname": "TikTokApi.api.sound.Sound.info", "modulename": "TikTokApi.api.sound", "qualname": "Sound.info", "type": "function", "doc": "

Returns a dictionary of TikTok's Sound/Music object.

\n\n
    \n
  • Parameters:\n
      \n
    • use_html (bool): If you want to perform an HTML request or not.\nDefaults to False to use an API call, which shouldn't get detected\nas often as an HTML request.
    • \n
  • \n
\n\n

Example Usage

\n\n
sound_data = api.sound(id='7016547803243022337').info()\n
\n", "signature": "(self, use_html=False, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.sound.Sound.info_full", "modulename": "TikTokApi.api.sound", "qualname": "Sound.info_full", "type": "function", "doc": "

Returns all the data associated with a TikTok Sound.

\n\n

This makes an API request, there is no HTML request option, as such\nwith Sound.info()

\n\n

Example Usage

\n\n
sound_data = api.sound(id='7016547803243022337').info_full()\n
\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.sound.Sound.videos", "modulename": "TikTokApi.api.sound", "qualname": "Sound.videos", "type": "function", "doc": "

Returns Video objects of videos created with this sound.

\n\n
    \n
  • Parameters:\n
      \n
    • count (int): The amount of videos you want returned.
    • \n
    • cursor (int): The unix epoch to get videos since. TODO: Check this is right
    • \n
  • \n
\n\n

Example Usage

\n\n
for video in api.sound(id='7016547803243022337').videos():\n    # do something\n
\n", "signature": "(\n self,\n count=30,\n offset=0,\n **kwargs\n) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]", "funcdef": "def"}, {"fullname": "TikTokApi.api.trending", "modulename": "TikTokApi.api.trending", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.trending.Trending", "modulename": "TikTokApi.api.trending", "qualname": "Trending", "type": "class", "doc": "

Contains static methods related to trending.

\n"}, {"fullname": "TikTokApi.api.trending.Trending.__init__", "modulename": "TikTokApi.api.trending", "qualname": "Trending.__init__", "type": "function", "doc": "

\n", "signature": "()", "funcdef": "def"}, {"fullname": "TikTokApi.api.trending.Trending.videos", "modulename": "TikTokApi.api.trending", "qualname": "Trending.videos", "type": "function", "doc": "

Returns Videos that are trending on TikTok.

\n\n
    \n
  • Parameters:\n
      \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n", "signature": "(\n count=30,\n **kwargs\n) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]", "funcdef": "def"}, {"fullname": "TikTokApi.api.user", "modulename": "TikTokApi.api.user", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.user.User", "modulename": "TikTokApi.api.user", "qualname": "User", "type": "class", "doc": "

A TikTok User.

\n\n

Example Usage

\n\n
user = api.user(username='therock')\n# or\nuser_id = '5831967'\nsec_uid = 'MS4wLjABAAAA-VASjiXTh7wDDyXvjk10VFhMWUAoxr8bgfO1kAL1-9s'\nuser = api.user(user_id=user_id, sec_uid=sec_uid)\n
\n"}, {"fullname": "TikTokApi.api.user.User.__init__", "modulename": "TikTokApi.api.user", "qualname": "User.__init__", "type": "function", "doc": "

You must provide the username or (user_id and sec_uid) otherwise this\nwill not function correctly.

\n", "signature": "(\n self,\n username: Optional[str] = None,\n user_id: Optional[str] = None,\n sec_uid: Optional[str] = None,\n data: Optional[str] = None\n)", "funcdef": "def"}, {"fullname": "TikTokApi.api.user.User.user_id", "modulename": "TikTokApi.api.user", "qualname": "User.user_id", "type": "variable", "doc": "

The user ID of the user.

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.user.User.sec_uid", "modulename": "TikTokApi.api.user", "qualname": "User.sec_uid", "type": "variable", "doc": "

The sec UID of the user.

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.user.User.username", "modulename": "TikTokApi.api.user", "qualname": "User.username", "type": "variable", "doc": "

The username of the user.

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.user.User.as_dict", "modulename": "TikTokApi.api.user", "qualname": "User.as_dict", "type": "variable", "doc": "

The raw data associated with this user.

\n", "annotation": ": dict"}, {"fullname": "TikTokApi.api.user.User.info", "modulename": "TikTokApi.api.user", "qualname": "User.info", "type": "function", "doc": "

Returns a dictionary of TikTok's User object

\n\n

Example Usage

\n\n
user_data = api.user(username='therock').info()\n
\n", "signature": "(self, **kwargs)", "funcdef": "def"}, {"fullname": "TikTokApi.api.user.User.info_full", "modulename": "TikTokApi.api.user", "qualname": "User.info_full", "type": "function", "doc": "

Returns a dictionary of information associated with this User.\nIncludes statistics about this user.

\n\n

Example Usage

\n\n
user_data = api.user(username='therock').info_full()\n
\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.user.User.videos", "modulename": "TikTokApi.api.user", "qualname": "User.videos", "type": "function", "doc": "

Returns a Generator yielding Video objects.

\n\n
    \n
  • Parameters:\n
      \n
    • count (int): The amount of videos you want returned.
    • \n
    • cursor (int): The unix epoch to get videos since. TODO: Check this is right
    • \n
  • \n
\n\n

Example Usage

\n\n
user = api.user(username='therock')\nfor video in user.videos(count=100):\n    print(video.id)\n
\n", "signature": "(\n self,\n count=30,\n cursor=0,\n **kwargs\n) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]", "funcdef": "def"}, {"fullname": "TikTokApi.api.user.User.liked", "modulename": "TikTokApi.api.user", "qualname": "User.liked", "type": "function", "doc": "

Returns a dictionary listing TikToks that a given a user has liked.

\n\n

Note: The user's likes must be public (which is not the default option)

\n\n
    \n
  • Parameters:\n
      \n
    • count (int): The amount of videos you want returned.
    • \n
    • cursor (int): The unix epoch to get videos since. TODO: Check this is right
    • \n
  • \n
\n\n

Example Usage

\n\n
for liked_video in api.user(username='public_likes'):\n    print(liked_video.id)\n
\n", "signature": "(\n self,\n count: int = 30,\n cursor: int = 0,\n **kwargs\n) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]", "funcdef": "def"}, {"fullname": "TikTokApi.api.video", "modulename": "TikTokApi.api.video", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.video.Video", "modulename": "TikTokApi.api.video", "qualname": "Video", "type": "class", "doc": "

A TikTok Video class

\n\n

Example Usage

\n\n
video = api.video(id='7041997751718137094')\n
\n"}, {"fullname": "TikTokApi.api.video.Video.__init__", "modulename": "TikTokApi.api.video", "qualname": "Video.__init__", "type": "function", "doc": "

You must provide the id or a valid url, else this will fail.

\n", "signature": "(\n self,\n id: Optional[str] = None,\n url: Optional[str] = None,\n data: Optional[dict] = None\n)", "funcdef": "def"}, {"fullname": "TikTokApi.api.video.Video.id", "modulename": "TikTokApi.api.video", "qualname": "Video.id", "type": "variable", "doc": "

TikTok's ID of the Video

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.video.Video.author", "modulename": "TikTokApi.api.video", "qualname": "Video.author", "type": "variable", "doc": "

The User who created the Video

\n", "annotation": ": Optional[TikTokApi.api.user.User]"}, {"fullname": "TikTokApi.api.video.Video.sound", "modulename": "TikTokApi.api.video", "qualname": "Video.sound", "type": "variable", "doc": "

The Sound that is associated with the Video

\n", "annotation": ": Optional[TikTokApi.api.sound.Sound]"}, {"fullname": "TikTokApi.api.video.Video.hashtags", "modulename": "TikTokApi.api.video", "qualname": "Video.hashtags", "type": "variable", "doc": "

A List of Hashtags on the Video

\n", "annotation": ": Optional[list[TikTokApi.api.hashtag.Hashtag]]"}, {"fullname": "TikTokApi.api.video.Video.as_dict", "modulename": "TikTokApi.api.video", "qualname": "Video.as_dict", "type": "variable", "doc": "

The raw data associated with this Video.

\n", "annotation": ": dict"}, {"fullname": "TikTokApi.api.video.Video.info", "modulename": "TikTokApi.api.video", "qualname": "Video.info", "type": "function", "doc": "

Returns a dictionary of TikTok's Video object.

\n\n

Example Usage

\n\n
video_data = api.video(id='7041997751718137094').info()\n
\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.video.Video.info_full", "modulename": "TikTokApi.api.video", "qualname": "Video.info_full", "type": "function", "doc": "

Returns a dictionary of all data associated with a TikTok Video.

\n\n

Example Usage

\n\n
video_data = api.video(id='7041997751718137094').info_full()\n
\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.video.Video.bytes", "modulename": "TikTokApi.api.video", "qualname": "Video.bytes", "type": "function", "doc": "

Returns the bytes of a TikTok Video.

\n\n

Example Usage

\n\n
video_bytes = api.video(id='7041997751718137094').bytes()\n\n# Saving The Video\nwith open('saved_video.mp4', 'wb') as output:\n    output.write(video_bytes)\n
\n", "signature": "(self, **kwargs) -> bytes", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities", "modulename": "TikTokApi.browser_utilities", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.browser_utilities.browser", "modulename": "TikTokApi.browser_utilities.browser", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.browser_utilities.browser.get_playwright", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "get_playwright", "type": "function", "doc": "

\n", "signature": "()", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser.browser", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser", "type": "class", "doc": "

Helper class that provides a standard way to create an ABC using\ninheritance.

\n", "bases": "TikTokApi.browser_utilities.browser_interface.BrowserInterface"}, {"fullname": "TikTokApi.browser_utilities.browser.browser.__init__", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.__init__", "type": "function", "doc": "

\n", "signature": "(self, **kwargs)", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser.browser.get_params", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.get_params", "type": "function", "doc": "

\n", "signature": "(self, page) -> None", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser.browser.gen_verifyFp", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.gen_verifyFp", "type": "function", "doc": "

\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser.browser.sign_url", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.sign_url", "type": "function", "doc": "

\n", "signature": "(self, url, calc_tt_params=False, **kwargs)", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser.browser.find_redirect", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.find_redirect", "type": "function", "doc": "

\n", "signature": "(self, url)", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser_interface", "modulename": "TikTokApi.browser_utilities.browser_interface", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.browser_utilities.browser_interface.BrowserInterface", "modulename": "TikTokApi.browser_utilities.browser_interface", "qualname": "BrowserInterface", "type": "class", "doc": "

Helper class that provides a standard way to create an ABC using\ninheritance.

\n", "bases": "abc.ABC"}, {"fullname": "TikTokApi.browser_utilities.browser_interface.BrowserInterface.get_params", "modulename": "TikTokApi.browser_utilities.browser_interface", "qualname": "BrowserInterface.get_params", "type": "function", "doc": "

\n", "signature": "(self, page) -> None", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser_interface.BrowserInterface.sign_url", "modulename": "TikTokApi.browser_utilities.browser_interface", "qualname": "BrowserInterface.sign_url", "type": "function", "doc": "

\n", "signature": "(self, calc_tt_params=False, **kwargs)", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.get_acrawler", "modulename": "TikTokApi.browser_utilities.get_acrawler", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.exceptions", "modulename": "TikTokApi.exceptions", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.exceptions.TikTokCaptchaError", "modulename": "TikTokApi.exceptions", "qualname": "TikTokCaptchaError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.TikTokCaptchaError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "TikTokCaptchaError.__init__", "type": "function", "doc": "

\n", "signature": "(\n self,\n message='TikTok blocks this request displaying a Captcha \\nTip: Consider using a proxy or a custom_verify_fp as method parameters'\n)", "funcdef": "def"}, {"fullname": "TikTokApi.exceptions.GenericTikTokError", "modulename": "TikTokApi.exceptions", "qualname": "GenericTikTokError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.GenericTikTokError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "GenericTikTokError.__init__", "type": "function", "doc": "

\n", "signature": "(self, message)", "funcdef": "def"}, {"fullname": "TikTokApi.exceptions.TikTokNotFoundError", "modulename": "TikTokApi.exceptions", "qualname": "TikTokNotFoundError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.TikTokNotFoundError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "TikTokNotFoundError.__init__", "type": "function", "doc": "

\n", "signature": "(self, message='The requested object does not exists')", "funcdef": "def"}, {"fullname": "TikTokApi.exceptions.EmptyResponseError", "modulename": "TikTokApi.exceptions", "qualname": "EmptyResponseError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.EmptyResponseError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "EmptyResponseError.__init__", "type": "function", "doc": "

\n", "signature": "(self, message='TikTok sent no data back')", "funcdef": "def"}, {"fullname": "TikTokApi.exceptions.JSONDecodeFailure", "modulename": "TikTokApi.exceptions", "qualname": "JSONDecodeFailure", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.JSONDecodeFailure.__init__", "modulename": "TikTokApi.exceptions", "qualname": "JSONDecodeFailure.__init__", "type": "function", "doc": "

\n", "signature": "(self, message='TikTok sent invalid JSON back')", "funcdef": "def"}, {"fullname": "TikTokApi.exceptions.TikTokNotAvailableError", "modulename": "TikTokApi.exceptions", "qualname": "TikTokNotAvailableError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.TikTokNotAvailableError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "TikTokNotAvailableError.__init__", "type": "function", "doc": "

\n", "signature": "(self, message='The requested object is not available in this region')", "funcdef": "def"}, {"fullname": "TikTokApi.helpers", "modulename": "TikTokApi.helpers", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.helpers.extract_tag_contents", "modulename": "TikTokApi.helpers", "qualname": "extract_tag_contents", "type": "function", "doc": "

\n", "signature": "(html)", "funcdef": "def"}, {"fullname": "TikTokApi.helpers.extract_video_id_from_url", "modulename": "TikTokApi.helpers", "qualname": "extract_video_id_from_url", "type": "function", "doc": "

\n", "signature": "(url)", "funcdef": "def"}, {"fullname": "TikTokApi.tiktok", "modulename": "TikTokApi.tiktok", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi", "type": "class", "doc": "

\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.__init__", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.__init__", "type": "function", "doc": "

The TikTokApi class. Used to interact with TikTok. This is a singleton\n class to prevent issues from arising with playwright

\n\n
Parameters
\n\n
    \n
  • logging_level: The logging level you want the program to run at, optional\nThese are the standard python logging module's levels.

  • \n
  • request_delay: The amount of time in seconds to wait before making a request, optional\nThis is used to throttle your own requests as you may end up making too\nmany requests to TikTok for your IP.

  • \n
  • custom_device_id: A TikTok parameter needed to download videos, optional\nThe code generates these and handles these pretty well itself, however\nfor some things such as video download you will need to set a consistent\none of these. All the methods take this as a optional parameter, however\nit's cleaner code to store this at the instance level. You can override\nthis at the specific methods.

  • \n
  • generate_static_device_id: A parameter that generates a custom_device_id at the instance level\nUse this if you want to download videos from a script but don't want to generate\nyour own custom_device_id parameter.

  • \n
  • custom_verify_fp: A TikTok parameter needed to work most of the time, optional\nTo get this parameter look at this video\nI recommend watching the entire thing, as it will help setup this package. All\nthe methods take this as a optional parameter, however it's cleaner code\nto store this at the instance level. You can override this at the specific\nmethods.

    \n\n

    You can use the following to generate \"\".join(random.choice(string.digits)\nfor num in range(19))

  • \n
  • use_test_endpoints: Send requests to TikTok's test endpoints, optional\nThis parameter when set to true will make requests to TikTok's testing\nendpoints instead of the live site. I can't guarantee this will work\nin the future, however currently basically any custom_verify_fp will\nwork here which is helpful.

  • \n
  • proxy: A string containing your proxy address, optional\nIf you want to do a lot of scraping of TikTok endpoints you'll likely\nneed a proxy.

    \n\n

    Ex: \"https://0.0.0.0:8080\"

    \n\n

    All the methods take this as a optional parameter, however it's cleaner code\nto store this at the instance level. You can override this at the specific\nmethods.

  • \n
  • use_selenium: Option to use selenium over playwright, optional\nPlaywright is selected by default and is the one that I'm designing the\npackage to be compatable for, however if playwright doesn't work on\nyour machine feel free to set this to True.

  • \n
  • executablePath: The location of the driver, optional\nThis shouldn't be needed if you're using playwright

  • \n
  • **kwargs\nParameters that are passed on to basically every module and methods\nthat interact with this main class. These may or may not be documented\nin other places.

  • \n
\n", "signature": "(cls, logging_level=30, *args, **kwargs)", "funcdef": "def"}, {"fullname": "TikTokApi.tiktok.TikTokApi.logger", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.logger", "type": "variable", "doc": "

\n", "annotation": ": ClassVar[logging.Logger]", "default_value": " = "}, {"fullname": "TikTokApi.tiktok.TikTokApi.user", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.user", "type": "class", "doc": "

A TikTok User.

\n\n

Example Usage

\n\n
user = api.user(username='therock')\n# or\nuser_id = '5831967'\nsec_uid = 'MS4wLjABAAAA-VASjiXTh7wDDyXvjk10VFhMWUAoxr8bgfO1kAL1-9s'\nuser = api.user(user_id=user_id, sec_uid=sec_uid)\n
\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.search", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.search", "type": "class", "doc": "

Contains static methods about searching.

\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.sound", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.sound", "type": "class", "doc": "

A TikTok Sound/Music/Song.

\n\n

Example Usage

\n\n
song = api.song(id='7016547803243022337')\n
\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.hashtag", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.hashtag", "type": "class", "doc": "

A TikTok Hashtag/Challenge.

\n\n

Example Usage

\n\n
hashtag = api.hashtag(name='funny')\n
\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.video", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.video", "type": "class", "doc": "

A TikTok Video class

\n\n

Example Usage

\n\n
video = api.video(id='7041997751718137094')\n
\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.trending", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.trending", "type": "class", "doc": "

Contains static methods related to trending.

\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.get_data", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_data", "type": "function", "doc": "

Makes requests to TikTok and returns their JSON.

\n\n

This is all handled by the package so it's unlikely\nyou will need to use this.

\n", "signature": "(self, path, use_desktop_base_url=False, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.tiktok.TikTokApi.external_signer", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.external_signer", "type": "function", "doc": "

Makes requests to an external signer instead of using a browser.

\n\n
Parameters
\n\n
    \n
  • url: The server to make requests to\nThis server is designed to sign requests. You can find an example\nof this signature server in the examples folder.

  • \n
  • custom_device_id: A TikTok parameter needed to download videos\nThe code generates these and handles these pretty well itself, however\nfor some things such as video download you will need to set a consistent\none of these.

  • \n
  • custom_verify_fp: A TikTok parameter needed to work most of the time,\nTo get this parameter look at this video\nI recommend watching the entire thing, as it will help setup this package.

  • \n
\n", "signature": "(self, url, custom_device_id=None, verifyFp=None)", "funcdef": "def"}, {"fullname": "TikTokApi.tiktok.TikTokApi.get_bytes", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_bytes", "type": "function", "doc": "

Returns TikTok's response as bytes, similar to get_data

\n", "signature": "(self, **kwargs) -> bytes", "funcdef": "def"}, {"fullname": "TikTokApi.tiktok.TikTokApi.generate_device_id", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.generate_device_id", "type": "function", "doc": "

Generates a valid device_id for other methods. Pass this as the custom_device_id field to download videos

\n", "signature": "()", "funcdef": "def"}, {"fullname": "TikTokApi.utilities", "modulename": "TikTokApi.utilities", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.utilities.LOGGER_NAME", "modulename": "TikTokApi.utilities", "qualname": "LOGGER_NAME", "type": "variable", "doc": "

\n", "annotation": ": str", "default_value": " = 'TikTokApi'"}, {"fullname": "TikTokApi.utilities.update_messager", "modulename": "TikTokApi.utilities", "qualname": "update_messager", "type": "function", "doc": "

\n", "signature": "()", "funcdef": "def"}, {"fullname": "TikTokApi.utilities.check", "modulename": "TikTokApi.utilities", "qualname": "check", "type": "function", "doc": "

\n", "signature": "(name)", "funcdef": "def"}, {"fullname": "TikTokApi.utilities.check_future_deprecation", "modulename": "TikTokApi.utilities", "qualname": "check_future_deprecation", "type": "function", "doc": "

\n", "signature": "()", "funcdef": "def"}]; + + // mirrored in build-search-index.js (part 1) + // Also split on html tags. this is a cheap heuristic, but good enough. + elasticlunr.tokenizer.setSeperator(/[\s\-.;&_'"=,()]+|<[^>]*>/); + + let searchIndex; + if (docs._isPrebuiltIndex) { + console.info("using precompiled search index"); + searchIndex = elasticlunr.Index.load(docs); + } else { + console.time("building search index"); + // mirrored in build-search-index.js (part 2) + searchIndex = elasticlunr(function () { + this.pipeline.remove(elasticlunr.stemmer); + this.pipeline.remove(elasticlunr.stopWordFilter); + this.addField("qualname"); + this.addField("fullname"); + this.addField("annotation"); + this.addField("default_value"); + this.addField("signature"); + this.addField("bases"); + this.addField("doc"); + this.setRef("fullname"); + }); + for (let doc of docs) { + searchIndex.addDoc(doc); + } + console.timeEnd("building search index"); + } + + return (term) => searchIndex.search(term, { + fields: { + qualname: {boost: 4}, + fullname: {boost: 2}, + annotation: {boost: 2}, + default_value: {boost: 2}, + signature: {boost: 2}, + bases: {boost: 2}, + doc: {boost: 1}, + }, + expand: true + }); +})(); \ No newline at end of file diff --git a/setup.py b/setup.py index fa0eeeb0..ecf6f24e 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ long_description_content_type="text/markdown", download_url="https://github.com/davidteather/TikTok-Api/tarball/master", keywords=["tiktok", "python3", "api", "unofficial", "tiktok-api", "tiktok api"], - install_requires=["requests", "playwright", "selenium_stealth", "selenium"], + install_requires=["requests", "playwright"], classifiers=[ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", From a5787ae20baf02412a4951496ffc5b422a5fb926 Mon Sep 17 00:00:00 2001 From: davidteather Date: Tue, 25 Jan 2022 21:14:43 -0600 Subject: [PATCH 11/17] Fix some mypy typing issues --- .github/ISSUE_TEMPLATE/bug_report.md | 10 +-- .github/ISSUE_TEMPLATE/installation-help.md | 3 + README.md | 2 +- TikTokApi/api/hashtag.py | 16 ++-- TikTokApi/api/search.py | 20 ++--- TikTokApi/api/sound.py | 4 +- TikTokApi/api/trending.py | 4 +- TikTokApi/api/user.py | 12 ++- TikTokApi/api/video.py | 2 +- TikTokApi/browser_utilities/browser.py | 4 +- TikTokApi/tiktok.py | 2 +- docs/TikTokApi.html | 2 +- docs/TikTokApi/api/hashtag.html | 48 +++++++----- docs/TikTokApi/api/search.html | 75 ++++++++++--------- docs/TikTokApi/api/sound.html | 10 +-- docs/TikTokApi/api/trending.html | 13 ++-- docs/TikTokApi/api/user.html | 40 +++++----- docs/TikTokApi/api/video.html | 6 +- docs/TikTokApi/browser_utilities/browser.html | 8 +- docs/TikTokApi/tiktok.html | 52 ++++++------- docs/search.js | 2 +- 21 files changed, 173 insertions(+), 162 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 10de3c52..efaded62 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,9 +6,7 @@ labels: bug assignees: '' --- - -# Read Below!!! If this doesn't fix your issue delete these two lines -**You may need to install chromedriver for your machine globally. Download it [here](https://sites.google.com/a/chromium.org/chromedriver/) and add it to your path.** +Fill Out the template :) **Describe the bug** @@ -16,7 +14,9 @@ A clear and concise description of what the bug is. **The buggy code** -Please insert the code that is throwing errors or is giving you weird unexpected results. +Please add any relevant code that is giving you unexpected results. + +Preferably the smallest amount of code to reproduce the issue. ``` # Code Goes Here @@ -35,7 +35,7 @@ Put the error trace below if there's any error thrown. **Desktop (please complete the following information):** - OS: [e.g. Windows 10] - - TikTokApi Version [e.g. 3.3.1] - if out of date upgrade before posting an issue + - TikTokApi Version [e.g. 5.0.0] - if out of date upgrade before posting an issue **Additional context** diff --git a/.github/ISSUE_TEMPLATE/installation-help.md b/.github/ISSUE_TEMPLATE/installation-help.md index 182b201f..caeea644 100644 --- a/.github/ISSUE_TEMPLATE/installation-help.md +++ b/.github/ISSUE_TEMPLATE/installation-help.md @@ -8,6 +8,9 @@ assignees: '' --- +Please first check the closed issues on GitHub for people with similar problems to you. +If you'd like more instant help from the community consider joining the [discord](https://discord.gg/yyPhbfma6f) + **Describe the error** Put the error trace here. diff --git a/README.md b/README.md index e4b2fa7d..4409116e 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ This package has been difficult to maintain due to it's structure, difficult to As a result, I've decided to at least attempt to remedy some of these issues, the biggest changes are that 1. The package has shifted to using classes for different TikTok objects resulting in an easier, higher-level programming experience. -2. All methods that used to return a list of objects have been switched to using generators, to hopefully decrease memory utilization for most users. +2. All methods that used to return a list of objects have been switched to using iterators, to hopefully decrease memory utilization for most users. ### Upgrading Examples diff --git a/TikTokApi/api/hashtag.py b/TikTokApi/api/hashtag.py index 75dc7d17..3958d320 100644 --- a/TikTokApi/api/hashtag.py +++ b/TikTokApi/api/hashtag.py @@ -4,7 +4,7 @@ from urllib.parse import urlencode from ..exceptions import * -from typing import TYPE_CHECKING, ClassVar, Generator, Optional +from typing import TYPE_CHECKING, ClassVar, Iterator, Optional if TYPE_CHECKING: from ..tiktok import TikTokApi @@ -23,9 +23,9 @@ class Hashtag: parent: ClassVar[TikTokApi] - id: str + id: Optional[str] """The ID of the hashtag""" - name: str + name: Optional[str] """The name of the hashtag (omiting the #)""" as_dict: dict """The raw data associated with this hashtag.""" @@ -34,7 +34,7 @@ def __init__( self, name: Optional[str] = None, id: Optional[str] = None, - data: Optional[str] = None, + data: Optional[dict] = None, ): """ You must provide the name or id of the hashtag. @@ -72,8 +72,12 @@ def info_full(self, **kwargs) -> dict: if self.name is not None: query = {"challengeName": self.name} - else: + elif self.id is not None: query = {"challengeId": self.id} + else: + self.parent.logger.warning("Malformed Hashtag Object") + return {} + path = "api/challenge/detail/?{}&{}".format( self.parent._add_url_params(), urlencode(query) ) @@ -85,7 +89,7 @@ def info_full(self, **kwargs) -> dict: return data - def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]: + def videos(self, count=30, offset=0, **kwargs) -> Iterator[Video]: """Returns a dictionary listing TikToks with a specific hashtag. - Parameters: diff --git a/TikTokApi/api/search.py b/TikTokApi/api/search.py index abf52a7b..6dd3a2d1 100644 --- a/TikTokApi/api/search.py +++ b/TikTokApi/api/search.py @@ -2,7 +2,7 @@ from urllib.parse import urlencode -from typing import TYPE_CHECKING, Generator +from typing import TYPE_CHECKING, Iterator from .user import User from .sound import Sound @@ -20,7 +20,7 @@ class Search: parent: TikTokApi @staticmethod - def users(search_term, count=28, **kwargs) -> Generator[User, None, None]: + def users(search_term, count=28, **kwargs) -> Iterator[User]: """ Searches for users. @@ -37,7 +37,7 @@ def users(search_term, count=28, **kwargs) -> Generator[User, None, None]: return Search.discover_type(search_term, prefix="user", count=count, **kwargs) @staticmethod - def sounds(search_term, count=28, **kwargs) -> Generator[Sound, None, None]: + def sounds(search_term, count=28, **kwargs) -> Iterator[Sound]: """ Searches for sounds. @@ -54,7 +54,7 @@ def sounds(search_term, count=28, **kwargs) -> Generator[Sound, None, None]: return Search.discover_type(search_term, prefix="music", count=count, **kwargs) @staticmethod - def hashtags(search_term, count=28, **kwargs) -> Generator[Hashtag, None, None]: + def hashtags(search_term, count=28, **kwargs) -> Iterator[Hashtag]: """ Searches for hashtags/challenges. @@ -73,7 +73,7 @@ def hashtags(search_term, count=28, **kwargs) -> Generator[Hashtag, None, None]: ) @staticmethod - def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> list: + def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> Iterator: """ Searches for a specific type of object. You should instead use the users/sounds/hashtags as they all use data @@ -137,7 +137,7 @@ def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> list: offset = int(data["offset"]) @staticmethod - def users_alternate(search_term, count=28, offset=0, **kwargs) -> list: + def users_alternate(search_term, count=28, offset=0, **kwargs) -> Iterator[User]: """ Searches for users using an alternate endpoint than Search.users @@ -180,18 +180,18 @@ def users_alternate(search_term, count=28, offset=0, **kwargs) -> list: "user", Search.parent._add_url_params(), urlencode(query) ) - data = Search.parent.get_data( + api_response = Search.parent.get_data( path, use_desktop_base_url=True, ttwid=ttwid, **kwargs ) # When I move to 3.10+ support make this a match switch. - for result in data.get("user_list", []): + for result in api_response.get("user_list", []): yield User(data=result) - if data.get("has_more", 0) == 0: + if api_response.get("has_more", 0) == 0: Search.parent.logger.info( "TikTok is not sending videos beyond this point." ) return - cursor = int(data.get("cursor")) + cursor = int(api_response.get("cursor", cursor)) diff --git a/TikTokApi/api/sound.py b/TikTokApi/api/sound.py index f2d66958..e016fac5 100644 --- a/TikTokApi/api/sound.py +++ b/TikTokApi/api/sound.py @@ -9,7 +9,7 @@ from ..helpers import extract_tag_contents from ..exceptions import * -from typing import TYPE_CHECKING, ClassVar, Generator, Optional +from typing import TYPE_CHECKING, ClassVar, Iterator, Optional if TYPE_CHECKING: from ..tiktok import TikTokApi @@ -111,7 +111,7 @@ def info_full(self, **kwargs) -> dict: data = extract_tag_contents(r.text) return json.loads(data)["props"]["pageProps"]["musicInfo"] - def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]: + def videos(self, count=30, offset=0, **kwargs) -> Iterator[Video]: """ Returns Video objects of videos created with this sound. diff --git a/TikTokApi/api/trending.py b/TikTokApi/api/trending.py index b0a21e1c..46431d94 100644 --- a/TikTokApi/api/trending.py +++ b/TikTokApi/api/trending.py @@ -6,7 +6,7 @@ from .video import Video -from typing import TYPE_CHECKING, Generator +from typing import TYPE_CHECKING, Iterator if TYPE_CHECKING: from ..tiktok import TikTokApi @@ -18,7 +18,7 @@ class Trending: parent: TikTokApi @staticmethod - def videos(count=30, **kwargs) -> Generator[Video, None, None]: + def videos(count=30, **kwargs) -> Iterator[Video]: """ Returns Videos that are trending on TikTok. diff --git a/TikTokApi/api/user.py b/TikTokApi/api/user.py index cafa8f42..7000441f 100644 --- a/TikTokApi/api/user.py +++ b/TikTokApi/api/user.py @@ -8,7 +8,7 @@ from ..exceptions import * from ..helpers import extract_tag_contents -from typing import TYPE_CHECKING, ClassVar, Generator, Optional +from typing import TYPE_CHECKING, ClassVar, Iterator, Optional if TYPE_CHECKING: from ..tiktok import TikTokApi @@ -46,7 +46,7 @@ def __init__( username: Optional[str] = None, user_id: Optional[str] = None, sec_uid: Optional[str] = None, - data: Optional[str] = None, + data: Optional[dict] = None, ): """ You must provide the username or (user_id and sec_uid) otherwise this @@ -111,9 +111,9 @@ def info_full(self, **kwargs) -> dict: return user_props["userInfo"] - def videos(self, count=30, cursor=0, **kwargs) -> Generator[Video, None, None]: + def videos(self, count=30, cursor=0, **kwargs) -> Iterator[Video]: """ - Returns a Generator yielding Video objects. + Returns an iterator yielding Video objects. - Parameters: - count (int): The amount of videos you want returned. @@ -174,9 +174,7 @@ def videos(self, count=30, cursor=0, **kwargs) -> Generator[Video, None, None]: cursor = res["cursor"] first = False - def liked( - self, count: int = 30, cursor: int = 0, **kwargs - ) -> Generator[Video, None, None]: + def liked(self, count: int = 30, cursor: int = 0, **kwargs) -> Iterator[Video]: """ Returns a dictionary listing TikToks that a given a user has liked. diff --git a/TikTokApi/api/video.py b/TikTokApi/api/video.py index 34a4e2cf..c0b9218e 100644 --- a/TikTokApi/api/video.py +++ b/TikTokApi/api/video.py @@ -26,7 +26,7 @@ class Video: parent: ClassVar[TikTokApi] - id: str + id: Optional[str] """TikTok's ID of the Video""" author: Optional[User] """The User who created the Video""" diff --git a/TikTokApi/browser_utilities/browser.py b/TikTokApi/browser_utilities/browser.py index 0fbd44dc..2e43d027 100644 --- a/TikTokApi/browser_utilities/browser.py +++ b/TikTokApi/browser_utilities/browser.py @@ -8,7 +8,7 @@ import json import re from .browser_interface import BrowserInterface -from urllib.parse import splitquery, parse_qs, parse_qsl +from urllib.parse import parse_qsl, urlparse from ..utilities import LOGGER_NAME from .get_acrawler import _get_acrawler, _get_tt_params_script @@ -232,7 +232,7 @@ def process(route): tt_params = page.evaluate( """() => { return window.genXTTParams(""" - + json.dumps(dict(parse_qsl(splitquery(url)[1]))) + + json.dumps(dict(parse_qsl(urlparse(url).query))) + """); }""" diff --git a/TikTokApi/tiktok.py b/TikTokApi/tiktok.py index 38c95eb4..637c5fbf 100644 --- a/TikTokApi/tiktok.py +++ b/TikTokApi/tiktok.py @@ -497,7 +497,7 @@ def generate_device_id(): # PRIVATE METHODS # - def _format_proxy(self, proxy) -> dict: + def _format_proxy(self, proxy) -> dict | None: """ Formats the proxy object """ diff --git a/docs/TikTokApi.html b/docs/TikTokApi.html index 13a1e9fe..16d367ee 100644 --- a/docs/TikTokApi.html +++ b/docs/TikTokApi.html @@ -174,7 +174,7 @@

Motivation

  1. The package has shifted to using classes for different TikTok objects resulting in an easier, higher-level programming experience.
  2. -
  3. All methods that used to return a list of objects have been switched to using generators, to hopefully decrease memory utilization for most users.
  4. +
  5. All methods that used to return a list of objects have been switched to using iterators, to hopefully decrease memory utilization for most users.

Upgrading Examples

diff --git a/docs/TikTokApi/api/hashtag.html b/docs/TikTokApi/api/hashtag.html index 6ddef760..2ec300f4 100644 --- a/docs/TikTokApi/api/hashtag.html +++ b/docs/TikTokApi/api/hashtag.html @@ -82,7 +82,7 @@

from urllib.parse import urlencode from ..exceptions import * -from typing import TYPE_CHECKING, ClassVar, Generator, Optional +from typing import TYPE_CHECKING, ClassVar, Iterator, Optional if TYPE_CHECKING: from ..tiktok import TikTokApi @@ -101,9 +101,9 @@

parent: ClassVar[TikTokApi] - id: str + id: Optional[str] """The ID of the hashtag""" - name: str + name: Optional[str] """The name of the hashtag (omiting the #)""" as_dict: dict """The raw data associated with this hashtag.""" @@ -112,7 +112,7 @@

self, name: Optional[str] = None, id: Optional[str] = None, - data: Optional[str] = None, + data: Optional[dict] = None, ): """ You must provide the name or id of the hashtag. @@ -150,8 +150,12 @@

if self.name is not None: query = {"challengeName": self.name} - else: + elif self.id is not None: query = {"challengeId": self.id} + else: + self.parent.logger.warning("Malformed Hashtag Object") + return {} + path = "api/challenge/detail/?{}&{}".format( self.parent._add_url_params(), urlencode(query) ) @@ -163,7 +167,7 @@

return data - def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]: + def videos(self, count=30, offset=0, **kwargs) -> Iterator[Video]: """Returns a dictionary listing TikToks with a specific hashtag. - Parameters: @@ -267,9 +271,9 @@

parent: ClassVar[TikTokApi] - id: str + id: Optional[str] """The ID of the hashtag""" - name: str + name: Optional[str] """The name of the hashtag (omiting the #)""" as_dict: dict """The raw data associated with this hashtag.""" @@ -278,7 +282,7 @@

self, name: Optional[str] = None, id: Optional[str] = None, - data: Optional[str] = None, + data: Optional[dict] = None, ): """ You must provide the name or id of the hashtag. @@ -316,8 +320,12 @@

if self.name is not None: query = {"challengeName": self.name} - else: + elif self.id is not None: query = {"challengeId": self.id} + else: + self.parent.logger.warning("Malformed Hashtag Object") + return {} + path = "api/challenge/detail/?{}&{}".format( self.parent._add_url_params(), urlencode(query) ) @@ -329,7 +337,7 @@

return data - def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]: + def videos(self, count=30, offset=0, **kwargs) -> Iterator[Video]: """Returns a dictionary listing TikToks with a specific hashtag. - Parameters: @@ -425,7 +433,7 @@

Hashtag( name: Optional[str] = None, id: Optional[str] = None, - data: Optional[str] = None + data: Optional[dict] = None ) @@ -435,7 +443,7 @@

self, name: Optional[str] = None, id: Optional[str] = None, - data: Optional[str] = None, + data: Optional[dict] = None, ): """ You must provide the name or id of the hashtag. @@ -458,7 +466,7 @@

#   - id: str + id: Optional[str]

The ID of the hashtag

@@ -469,7 +477,7 @@

#   - name: str + name: Optional[str]

The name of the hashtag (omiting the #)

@@ -542,8 +550,12 @@

if self.name is not None: query = {"challengeName": self.name} - else: + elif self.id is not None: query = {"challengeId": self.id} + else: + self.parent.logger.warning("Malformed Hashtag Object") + return {} + path = "api/challenge/detail/?{}&{}".format( self.parent._add_url_params(), urlencode(query) ) @@ -578,12 +590,12 @@

count=30, offset=0, **kwargs -) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]: +) -> Iterator[TikTokApi.api.video.Video]:

View Source -
    def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]:
+            
    def videos(self, count=30, offset=0, **kwargs) -> Iterator[Video]:
         """Returns a dictionary listing TikToks with a specific hashtag.
 
         - Parameters:
diff --git a/docs/TikTokApi/api/search.html b/docs/TikTokApi/api/search.html
index fd42e69a..8b78f717 100644
--- a/docs/TikTokApi/api/search.html
+++ b/docs/TikTokApi/api/search.html
@@ -77,7 +77,7 @@ 

from urllib.parse import urlencode -from typing import TYPE_CHECKING, Generator +from typing import TYPE_CHECKING, Iterator from .user import User from .sound import Sound @@ -95,7 +95,7 @@

parent: TikTokApi @staticmethod - def users(search_term, count=28, **kwargs) -> Generator[User, None, None]: + def users(search_term, count=28, **kwargs) -> Iterator[User]: """ Searches for users. @@ -112,7 +112,7 @@

return Search.discover_type(search_term, prefix="user", count=count, **kwargs) @staticmethod - def sounds(search_term, count=28, **kwargs) -> Generator[Sound, None, None]: + def sounds(search_term, count=28, **kwargs) -> Iterator[Sound]: """ Searches for sounds. @@ -129,7 +129,7 @@

return Search.discover_type(search_term, prefix="music", count=count, **kwargs) @staticmethod - def hashtags(search_term, count=28, **kwargs) -> Generator[Hashtag, None, None]: + def hashtags(search_term, count=28, **kwargs) -> Iterator[Hashtag]: """ Searches for hashtags/challenges. @@ -148,7 +148,7 @@

) @staticmethod - def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> list: + def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> Iterator: """ Searches for a specific type of object. You should instead use the users/sounds/hashtags as they all use data @@ -212,7 +212,7 @@

offset = int(data["offset"]) @staticmethod - def users_alternate(search_term, count=28, offset=0, **kwargs) -> list: + def users_alternate(search_term, count=28, offset=0, **kwargs) -> Iterator[User]: """ Searches for users using an alternate endpoint than Search.users @@ -255,21 +255,21 @@

"user", Search.parent._add_url_params(), urlencode(query) ) - data = Search.parent.get_data( + api_response = Search.parent.get_data( path, use_desktop_base_url=True, ttwid=ttwid, **kwargs ) # When I move to 3.10+ support make this a match switch. - for result in data.get("user_list", []): + for result in api_response.get("user_list", []): yield User(data=result) - if data.get("has_more", 0) == 0: + if api_response.get("has_more", 0) == 0: Search.parent.logger.info( "TikTok is not sending videos beyond this point." ) return - cursor = int(data.get("cursor")) + cursor = int(api_response.get("cursor", cursor))

@@ -292,7 +292,7 @@

parent: TikTokApi @staticmethod - def users(search_term, count=28, **kwargs) -> Generator[User, None, None]: + def users(search_term, count=28, **kwargs) -> Iterator[User]: """ Searches for users. @@ -309,7 +309,7 @@

return Search.discover_type(search_term, prefix="user", count=count, **kwargs) @staticmethod - def sounds(search_term, count=28, **kwargs) -> Generator[Sound, None, None]: + def sounds(search_term, count=28, **kwargs) -> Iterator[Sound]: """ Searches for sounds. @@ -326,7 +326,7 @@

return Search.discover_type(search_term, prefix="music", count=count, **kwargs) @staticmethod - def hashtags(search_term, count=28, **kwargs) -> Generator[Hashtag, None, None]: + def hashtags(search_term, count=28, **kwargs) -> Iterator[Hashtag]: """ Searches for hashtags/challenges. @@ -345,7 +345,7 @@

) @staticmethod - def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> list: + def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> Iterator: """ Searches for a specific type of object. You should instead use the users/sounds/hashtags as they all use data @@ -409,7 +409,7 @@

offset = int(data["offset"]) @staticmethod - def users_alternate(search_term, count=28, offset=0, **kwargs) -> list: + def users_alternate(search_term, count=28, offset=0, **kwargs) -> Iterator[User]: """ Searches for users using an alternate endpoint than Search.users @@ -452,21 +452,21 @@

"user", Search.parent._add_url_params(), urlencode(query) ) - data = Search.parent.get_data( + api_response = Search.parent.get_data( path, use_desktop_base_url=True, ttwid=ttwid, **kwargs ) # When I move to 3.10+ support make this a match switch. - for result in data.get("user_list", []): + for result in api_response.get("user_list", []): yield User(data=result) - if data.get("has_more", 0) == 0: + if api_response.get("has_more", 0) == 0: Search.parent.logger.info( "TikTok is not sending videos beyond this point." ) return - cursor = int(data.get("cursor")) + cursor = int(api_response.get("cursor", cursor))

@@ -492,17 +492,13 @@

@staticmethod
def - users( - search_term, - count=28, - **kwargs -) -> Generator[TikTokApi.api.user.User, NoneType, NoneType]: + users(search_term, count=28, **kwargs) -> Iterator[TikTokApi.api.user.User]:

View Source
    @staticmethod
-    def users(search_term, count=28, **kwargs) -> Generator[User, None, None]:
+    def users(search_term, count=28, **kwargs) -> Iterator[User]:
         """
         Searches for users.
 
@@ -550,13 +546,13 @@ 

search_term, count=28, **kwargs -) -> Generator[TikTokApi.api.sound.Sound, NoneType, NoneType]: +) -> Iterator[TikTokApi.api.sound.Sound]:

View Source
    @staticmethod
-    def sounds(search_term, count=28, **kwargs) -> Generator[Sound, None, None]:
+    def sounds(search_term, count=28, **kwargs) -> Iterator[Sound]:
         """
         Searches for sounds.
 
@@ -604,13 +600,13 @@ 

search_term, count=28, **kwargs -) -> Generator[TikTokApi.api.hashtag.Hashtag, NoneType, NoneType]: +) -> Iterator[TikTokApi.api.hashtag.Hashtag]:

View Source
    @staticmethod
-    def hashtags(search_term, count=28, **kwargs) -> Generator[Hashtag, None, None]:
+    def hashtags(search_term, count=28, **kwargs) -> Iterator[Hashtag]:
         """
         Searches for hashtags/challenges.
 
@@ -656,13 +652,13 @@ 

@staticmethod
def - discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> list: + discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> Iterator:

View Source
    @staticmethod
-    def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> list:
+    def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> Iterator:
         """
         Searches for a specific type of object.
         You should instead use the users/sounds/hashtags as they all use data
@@ -756,13 +752,18 @@ 

@staticmethod
def - users_alternate(search_term, count=28, offset=0, **kwargs) -> list: + users_alternate( + search_term, + count=28, + offset=0, + **kwargs +) -> Iterator[TikTokApi.api.user.User]:

View Source
    @staticmethod
-    def users_alternate(search_term, count=28, offset=0, **kwargs) -> list:
+    def users_alternate(search_term, count=28, offset=0, **kwargs) -> Iterator[User]:
         """
         Searches for users using an alternate endpoint than Search.users
 
@@ -805,21 +806,21 @@ 

"user", Search.parent._add_url_params(), urlencode(query) ) - data = Search.parent.get_data( + api_response = Search.parent.get_data( path, use_desktop_base_url=True, ttwid=ttwid, **kwargs ) # When I move to 3.10+ support make this a match switch. - for result in data.get("user_list", []): + for result in api_response.get("user_list", []): yield User(data=result) - if data.get("has_more", 0) == 0: + if api_response.get("has_more", 0) == 0: Search.parent.logger.info( "TikTok is not sending videos beyond this point." ) return - cursor = int(data.get("cursor")) + cursor = int(api_response.get("cursor", cursor))

diff --git a/docs/TikTokApi/api/sound.html b/docs/TikTokApi/api/sound.html index 23545be7..a540ee2f 100644 --- a/docs/TikTokApi/api/sound.html +++ b/docs/TikTokApi/api/sound.html @@ -87,7 +87,7 @@

from ..helpers import extract_tag_contents from ..exceptions import * -from typing import TYPE_CHECKING, ClassVar, Generator, Optional +from typing import TYPE_CHECKING, ClassVar, Iterator, Optional if TYPE_CHECKING: from ..tiktok import TikTokApi @@ -189,7 +189,7 @@

data = extract_tag_contents(r.text) return json.loads(data)["props"]["pageProps"]["musicInfo"] - def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]: + def videos(self, count=30, offset=0, **kwargs) -> Iterator[Video]: """ Returns Video objects of videos created with this sound. @@ -379,7 +379,7 @@

data = extract_tag_contents(r.text) return json.loads(data)["props"]["pageProps"]["musicInfo"] - def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]: + def videos(self, count=30, offset=0, **kwargs) -> Iterator[Video]: """ Returns Video objects of videos created with this sound. @@ -665,12 +665,12 @@

count=30, offset=0, **kwargs -) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]: +) -> Iterator[TikTokApi.api.video.Video]:

View Source -
    def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]:
+            
    def videos(self, count=30, offset=0, **kwargs) -> Iterator[Video]:
         """
         Returns Video objects of videos created with this sound.
 
diff --git a/docs/TikTokApi/api/trending.html b/docs/TikTokApi/api/trending.html
index 69d34c7d..eea510a8 100644
--- a/docs/TikTokApi/api/trending.html
+++ b/docs/TikTokApi/api/trending.html
@@ -69,7 +69,7 @@ 

from .video import Video -from typing import TYPE_CHECKING, Generator +from typing import TYPE_CHECKING, Iterator if TYPE_CHECKING: from ..tiktok import TikTokApi @@ -81,7 +81,7 @@

parent: TikTokApi @staticmethod - def videos(count=30, **kwargs) -> Generator[Video, None, None]: + def videos(count=30, **kwargs) -> Iterator[Video]: """ Returns Videos that are trending on TikTok. @@ -156,7 +156,7 @@

parent: TikTokApi @staticmethod - def videos(count=30, **kwargs) -> Generator[Video, None, None]: + def videos(count=30, **kwargs) -> Iterator[Video]: """ Returns Videos that are trending on TikTok. @@ -234,16 +234,13 @@

@staticmethod
def - videos( - count=30, - **kwargs -) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]: + videos(count=30, **kwargs) -> Iterator[TikTokApi.api.video.Video]:

View Source
    @staticmethod
-    def videos(count=30, **kwargs) -> Generator[Video, None, None]:
+    def videos(count=30, **kwargs) -> Iterator[Video]:
         """
         Returns Videos that are trending on TikTok.
 
diff --git a/docs/TikTokApi/api/user.html b/docs/TikTokApi/api/user.html
index 839a4f69..6741b480 100644
--- a/docs/TikTokApi/api/user.html
+++ b/docs/TikTokApi/api/user.html
@@ -92,7 +92,7 @@ 

from ..exceptions import * from ..helpers import extract_tag_contents -from typing import TYPE_CHECKING, ClassVar, Generator, Optional +from typing import TYPE_CHECKING, ClassVar, Iterator, Optional if TYPE_CHECKING: from ..tiktok import TikTokApi @@ -130,7 +130,7 @@

username: Optional[str] = None, user_id: Optional[str] = None, sec_uid: Optional[str] = None, - data: Optional[str] = None, + data: Optional[dict] = None, ): """ You must provide the username or (user_id and sec_uid) otherwise this @@ -195,9 +195,9 @@

return user_props["userInfo"] - def videos(self, count=30, cursor=0, **kwargs) -> Generator[Video, None, None]: + def videos(self, count=30, cursor=0, **kwargs) -> Iterator[Video]: """ - Returns a Generator yielding Video objects. + Returns an iterator yielding Video objects. - Parameters: - count (int): The amount of videos you want returned. @@ -258,9 +258,7 @@

cursor = res["cursor"] first = False - def liked( - self, count: int = 30, cursor: int = 0, **kwargs - ) -> Generator[Video, None, None]: + def liked(self, count: int = 30, cursor: int = 0, **kwargs) -> Iterator[Video]: """ Returns a dictionary listing TikToks that a given a user has liked. @@ -429,7 +427,7 @@

username: Optional[str] = None, user_id: Optional[str] = None, sec_uid: Optional[str] = None, - data: Optional[str] = None, + data: Optional[dict] = None, ): """ You must provide the username or (user_id and sec_uid) otherwise this @@ -494,9 +492,9 @@

return user_props["userInfo"] - def videos(self, count=30, cursor=0, **kwargs) -> Generator[Video, None, None]: + def videos(self, count=30, cursor=0, **kwargs) -> Iterator[Video]: """ - Returns a Generator yielding Video objects. + Returns an iterator yielding Video objects. - Parameters: - count (int): The amount of videos you want returned. @@ -557,9 +555,7 @@

cursor = res["cursor"] first = False - def liked( - self, count: int = 30, cursor: int = 0, **kwargs - ) -> Generator[Video, None, None]: + def liked(self, count: int = 30, cursor: int = 0, **kwargs) -> Iterator[Video]: """ Returns a dictionary listing TikToks that a given a user has liked. @@ -706,7 +702,7 @@

username: Optional[str] = None, user_id: Optional[str] = None, sec_uid: Optional[str] = None, - data: Optional[str] = None + data: Optional[dict] = None )

@@ -717,7 +713,7 @@

username: Optional[str] = None, user_id: Optional[str] = None, sec_uid: Optional[str] = None, - data: Optional[str] = None, + data: Optional[dict] = None, ): """ You must provide the username or (user_id and sec_uid) otherwise this @@ -892,14 +888,14 @@

count=30, cursor=0, **kwargs -) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]: +) -> Iterator[TikTokApi.api.video.Video]:

View Source -
    def videos(self, count=30, cursor=0, **kwargs) -> Generator[Video, None, None]:
+            
    def videos(self, count=30, cursor=0, **kwargs) -> Iterator[Video]:
         """
-        Returns a Generator yielding Video objects.
+        Returns an iterator yielding Video objects.
 
         - Parameters:
             - count (int): The amount of videos you want returned.
@@ -963,7 +959,7 @@ 

-

Returns a Generator yielding Video objects.

+

Returns an iterator yielding Video objects.

View Source -
    def liked(
-        self, count: int = 30, cursor: int = 0, **kwargs
-    ) -> Generator[Video, None, None]:
+            
    def liked(self, count: int = 30, cursor: int = 0, **kwargs) -> Iterator[Video]:
         """
         Returns a dictionary listing TikToks that a given a user has liked.
 
diff --git a/docs/TikTokApi/api/video.html b/docs/TikTokApi/api/video.html
index 77c4f996..b3148309 100644
--- a/docs/TikTokApi/api/video.html
+++ b/docs/TikTokApi/api/video.html
@@ -110,7 +110,7 @@ 

parent: ClassVar[TikTokApi] - id: str + id: Optional[str] """TikTok's ID of the Video""" author: Optional[User] """The User who created the Video""" @@ -267,7 +267,7 @@

parent: ClassVar[TikTokApi] - id: str + id: Optional[str] """TikTok's ID of the Video""" author: Optional[User] """The User who created the Video""" @@ -452,7 +452,7 @@

#   - id: str + id: Optional[str]

TikTok's ID of the Video

diff --git a/docs/TikTokApi/browser_utilities/browser.html b/docs/TikTokApi/browser_utilities/browser.html index 15eed1b6..c9010ce8 100644 --- a/docs/TikTokApi/browser_utilities/browser.html +++ b/docs/TikTokApi/browser_utilities/browser.html @@ -83,7 +83,7 @@

import json import re from .browser_interface import BrowserInterface -from urllib.parse import splitquery, parse_qs, parse_qsl +from urllib.parse import parse_qsl, urlparse from ..utilities import LOGGER_NAME from .get_acrawler import _get_acrawler, _get_tt_params_script @@ -307,7 +307,7 @@

tt_params = page.evaluate( """() => { return window.genXTTParams(""" - + json.dumps(dict(parse_qsl(splitquery(url)[1]))) + + json.dumps(dict(parse_qsl(urlparse(url).query))) + """); }""" @@ -582,7 +582,7 @@

tt_params = page.evaluate( """() => { return window.genXTTParams(""" - + json.dumps(dict(parse_qsl(splitquery(url)[1]))) + + json.dumps(dict(parse_qsl(urlparse(url).query))) + """); }""" @@ -844,7 +844,7 @@

tt_params = page.evaluate( """() => { return window.genXTTParams(""" - + json.dumps(dict(parse_qsl(splitquery(url)[1]))) + + json.dumps(dict(parse_qsl(urlparse(url).query))) + """); }""" diff --git a/docs/TikTokApi/tiktok.html b/docs/TikTokApi/tiktok.html index 30ed8af0..aa7c0534 100644 --- a/docs/TikTokApi/tiktok.html +++ b/docs/TikTokApi/tiktok.html @@ -590,7 +590,7 @@

# PRIVATE METHODS # - def _format_proxy(self, proxy) -> dict: + def _format_proxy(self, proxy) -> dict | None: """ Formats the proxy object """ @@ -1140,7 +1140,7 @@

# PRIVATE METHODS # - def _format_proxy(self, proxy) -> dict: + def _format_proxy(self, proxy) -> dict | None: """ Formats the proxy object """ @@ -1770,7 +1770,7 @@

Parameters
username: Optional[str] = None, user_id: Optional[str] = None, sec_uid: Optional[str] = None, - data: Optional[str] = None, + data: Optional[dict] = None, ): """ You must provide the username or (user_id and sec_uid) otherwise this @@ -1835,9 +1835,9 @@
Parameters
return user_props["userInfo"] - def videos(self, count=30, cursor=0, **kwargs) -> Generator[Video, None, None]: + def videos(self, count=30, cursor=0, **kwargs) -> Iterator[Video]: """ - Returns a Generator yielding Video objects. + Returns an iterator yielding Video objects. - Parameters: - count (int): The amount of videos you want returned. @@ -1898,9 +1898,7 @@
Parameters
cursor = res["cursor"] first = False - def liked( - self, count: int = 30, cursor: int = 0, **kwargs - ) -> Generator[Video, None, None]: + def liked(self, count: int = 30, cursor: int = 0, **kwargs) -> Iterator[Video]: """ Returns a dictionary listing TikToks that a given a user has liked. @@ -2074,7 +2072,7 @@
Inherited Members
parent: TikTokApi @staticmethod - def users(search_term, count=28, **kwargs) -> Generator[User, None, None]: + def users(search_term, count=28, **kwargs) -> Iterator[User]: """ Searches for users. @@ -2091,7 +2089,7 @@
Inherited Members
return Search.discover_type(search_term, prefix="user", count=count, **kwargs) @staticmethod - def sounds(search_term, count=28, **kwargs) -> Generator[Sound, None, None]: + def sounds(search_term, count=28, **kwargs) -> Iterator[Sound]: """ Searches for sounds. @@ -2108,7 +2106,7 @@
Inherited Members
return Search.discover_type(search_term, prefix="music", count=count, **kwargs) @staticmethod - def hashtags(search_term, count=28, **kwargs) -> Generator[Hashtag, None, None]: + def hashtags(search_term, count=28, **kwargs) -> Iterator[Hashtag]: """ Searches for hashtags/challenges. @@ -2127,7 +2125,7 @@
Inherited Members
) @staticmethod - def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> list: + def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> Iterator: """ Searches for a specific type of object. You should instead use the users/sounds/hashtags as they all use data @@ -2191,7 +2189,7 @@
Inherited Members
offset = int(data["offset"]) @staticmethod - def users_alternate(search_term, count=28, offset=0, **kwargs) -> list: + def users_alternate(search_term, count=28, offset=0, **kwargs) -> Iterator[User]: """ Searches for users using an alternate endpoint than Search.users @@ -2234,21 +2232,21 @@
Inherited Members
"user", Search.parent._add_url_params(), urlencode(query) ) - data = Search.parent.get_data( + api_response = Search.parent.get_data( path, use_desktop_base_url=True, ttwid=ttwid, **kwargs ) # When I move to 3.10+ support make this a match switch. - for result in data.get("user_list", []): + for result in api_response.get("user_list", []): yield User(data=result) - if data.get("has_more", 0) == 0: + if api_response.get("has_more", 0) == 0: Search.parent.logger.info( "TikTok is not sending videos beyond this point." ) return - cursor = int(data.get("cursor")) + cursor = int(api_response.get("cursor", cursor))

@@ -2377,7 +2375,7 @@
Inherited Members
data = extract_tag_contents(r.text) return json.loads(data)["props"]["pageProps"]["musicInfo"] - def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]: + def videos(self, count=30, offset=0, **kwargs) -> Iterator[Video]: """ Returns Video objects of videos created with this sound. @@ -2509,9 +2507,9 @@
Inherited Members
parent: ClassVar[TikTokApi] - id: str + id: Optional[str] """The ID of the hashtag""" - name: str + name: Optional[str] """The name of the hashtag (omiting the #)""" as_dict: dict """The raw data associated with this hashtag.""" @@ -2520,7 +2518,7 @@
Inherited Members
self, name: Optional[str] = None, id: Optional[str] = None, - data: Optional[str] = None, + data: Optional[dict] = None, ): """ You must provide the name or id of the hashtag. @@ -2558,8 +2556,12 @@
Inherited Members
if self.name is not None: query = {"challengeName": self.name} - else: + elif self.id is not None: query = {"challengeId": self.id} + else: + self.parent.logger.warning("Malformed Hashtag Object") + return {} + path = "api/challenge/detail/?{}&{}".format( self.parent._add_url_params(), urlencode(query) ) @@ -2571,7 +2573,7 @@
Inherited Members
return data - def videos(self, count=30, offset=0, **kwargs) -> Generator[Video, None, None]: + def videos(self, count=30, offset=0, **kwargs) -> Iterator[Video]: """Returns a dictionary listing TikToks with a specific hashtag. - Parameters: @@ -2699,7 +2701,7 @@
Inherited Members
parent: ClassVar[TikTokApi] - id: str + id: Optional[str] """TikTok's ID of the Video""" author: Optional[User] """The User who created the Video""" @@ -2876,7 +2878,7 @@
Inherited Members
parent: TikTokApi @staticmethod - def videos(count=30, **kwargs) -> Generator[Video, None, None]: + def videos(count=30, **kwargs) -> Iterator[Video]: """ Returns Videos that are trending on TikTok. diff --git a/docs/search.js b/docs/search.js index 3cc7ffe9..85f53bdf 100644 --- a/docs/search.js +++ b/docs/search.js @@ -1,6 +1,6 @@ window.pdocSearch = (function(){ /** elasticlunr - http://weixsong.github.io * Copyright (C) 2017 Oliver Nightingale * Copyright (C) 2017 Wei Song * MIT Licensed */!function(){function e(e){if(null===e||"object"!=typeof e)return e;var t=e.constructor();for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n]);return t}var t=function(e){var n=new t.Index;return n.pipeline.add(t.trimmer,t.stopWordFilter,t.stemmer),e&&e.call(n,n),n};t.version="0.9.5",lunr=t,t.utils={},t.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),t.utils.toString=function(e){return void 0===e||null===e?"":e.toString()},t.EventEmitter=function(){this.events={}},t.EventEmitter.prototype.addListener=function(){var e=Array.prototype.slice.call(arguments),t=e.pop(),n=e;if("function"!=typeof t)throw new TypeError("last argument must be a function");n.forEach(function(e){this.hasHandler(e)||(this.events[e]=[]),this.events[e].push(t)},this)},t.EventEmitter.prototype.removeListener=function(e,t){if(this.hasHandler(e)){var n=this.events[e].indexOf(t);-1!==n&&(this.events[e].splice(n,1),0==this.events[e].length&&delete this.events[e])}},t.EventEmitter.prototype.emit=function(e){if(this.hasHandler(e)){var t=Array.prototype.slice.call(arguments,1);this.events[e].forEach(function(e){e.apply(void 0,t)},this)}},t.EventEmitter.prototype.hasHandler=function(e){return e in this.events},t.tokenizer=function(e){if(!arguments.length||null===e||void 0===e)return[];if(Array.isArray(e)){var n=e.filter(function(e){return null===e||void 0===e?!1:!0});n=n.map(function(e){return t.utils.toString(e).toLowerCase()});var i=[];return n.forEach(function(e){var n=e.split(t.tokenizer.seperator);i=i.concat(n)},this),i}return e.toString().trim().toLowerCase().split(t.tokenizer.seperator)},t.tokenizer.defaultSeperator=/[\s\-]+/,t.tokenizer.seperator=t.tokenizer.defaultSeperator,t.tokenizer.setSeperator=function(e){null!==e&&void 0!==e&&"object"==typeof e&&(t.tokenizer.seperator=e)},t.tokenizer.resetSeperator=function(){t.tokenizer.seperator=t.tokenizer.defaultSeperator},t.tokenizer.getSeperator=function(){return t.tokenizer.seperator},t.Pipeline=function(){this._queue=[]},t.Pipeline.registeredFunctions={},t.Pipeline.registerFunction=function(e,n){n in t.Pipeline.registeredFunctions&&t.utils.warn("Overwriting existing registered function: "+n),e.label=n,t.Pipeline.registeredFunctions[n]=e},t.Pipeline.getRegisteredFunction=function(e){return e in t.Pipeline.registeredFunctions!=!0?null:t.Pipeline.registeredFunctions[e]},t.Pipeline.warnIfFunctionNotRegistered=function(e){var n=e.label&&e.label in this.registeredFunctions;n||t.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},t.Pipeline.load=function(e){var n=new t.Pipeline;return e.forEach(function(e){var i=t.Pipeline.getRegisteredFunction(e);if(!i)throw new Error("Cannot load un-registered function: "+e);n.add(i)}),n},t.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach(function(e){t.Pipeline.warnIfFunctionNotRegistered(e),this._queue.push(e)},this)},t.Pipeline.prototype.after=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i+1,0,n)},t.Pipeline.prototype.before=function(e,n){t.Pipeline.warnIfFunctionNotRegistered(n);var i=this._queue.indexOf(e);if(-1===i)throw new Error("Cannot find existingFn");this._queue.splice(i,0,n)},t.Pipeline.prototype.remove=function(e){var t=this._queue.indexOf(e);-1!==t&&this._queue.splice(t,1)},t.Pipeline.prototype.run=function(e){for(var t=[],n=e.length,i=this._queue.length,o=0;n>o;o++){for(var r=e[o],s=0;i>s&&(r=this._queue[s](r,o,e),void 0!==r&&null!==r);s++);void 0!==r&&null!==r&&t.push(r)}return t},t.Pipeline.prototype.reset=function(){this._queue=[]},t.Pipeline.prototype.get=function(){return this._queue},t.Pipeline.prototype.toJSON=function(){return this._queue.map(function(e){return t.Pipeline.warnIfFunctionNotRegistered(e),e.label})},t.Index=function(){this._fields=[],this._ref="id",this.pipeline=new t.Pipeline,this.documentStore=new t.DocumentStore,this.index={},this.eventEmitter=new t.EventEmitter,this._idfCache={},this.on("add","remove","update",function(){this._idfCache={}}.bind(this))},t.Index.prototype.on=function(){var e=Array.prototype.slice.call(arguments);return this.eventEmitter.addListener.apply(this.eventEmitter,e)},t.Index.prototype.off=function(e,t){return this.eventEmitter.removeListener(e,t)},t.Index.load=function(e){e.version!==t.version&&t.utils.warn("version mismatch: current "+t.version+" importing "+e.version);var n=new this;n._fields=e.fields,n._ref=e.ref,n.documentStore=t.DocumentStore.load(e.documentStore),n.pipeline=t.Pipeline.load(e.pipeline),n.index={};for(var i in e.index)n.index[i]=t.InvertedIndex.load(e.index[i]);return n},t.Index.prototype.addField=function(e){return this._fields.push(e),this.index[e]=new t.InvertedIndex,this},t.Index.prototype.setRef=function(e){return this._ref=e,this},t.Index.prototype.saveDocument=function(e){return this.documentStore=new t.DocumentStore(e),this},t.Index.prototype.addDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.addDoc(i,e),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));this.documentStore.addFieldLength(i,n,o.length);var r={};o.forEach(function(e){e in r?r[e]+=1:r[e]=1},this);for(var s in r){var u=r[s];u=Math.sqrt(u),this.index[n].addToken(s,{ref:i,tf:u})}},this),n&&this.eventEmitter.emit("add",e,this)}},t.Index.prototype.removeDocByRef=function(e){if(e&&this.documentStore.isDocStored()!==!1&&this.documentStore.hasDoc(e)){var t=this.documentStore.getDoc(e);this.removeDoc(t,!1)}},t.Index.prototype.removeDoc=function(e,n){if(e){var n=void 0===n?!0:n,i=e[this._ref];this.documentStore.hasDoc(i)&&(this.documentStore.removeDoc(i),this._fields.forEach(function(n){var o=this.pipeline.run(t.tokenizer(e[n]));o.forEach(function(e){this.index[n].removeToken(e,i)},this)},this),n&&this.eventEmitter.emit("remove",e,this))}},t.Index.prototype.updateDoc=function(e,t){var t=void 0===t?!0:t;this.removeDocByRef(e[this._ref],!1),this.addDoc(e,!1),t&&this.eventEmitter.emit("update",e,this)},t.Index.prototype.idf=function(e,t){var n="@"+t+"/"+e;if(Object.prototype.hasOwnProperty.call(this._idfCache,n))return this._idfCache[n];var i=this.index[t].getDocFreq(e),o=1+Math.log(this.documentStore.length/(i+1));return this._idfCache[n]=o,o},t.Index.prototype.getFields=function(){return this._fields.slice()},t.Index.prototype.search=function(e,n){if(!e)return[];e="string"==typeof e?{any:e}:JSON.parse(JSON.stringify(e));var i=null;null!=n&&(i=JSON.stringify(n));for(var o=new t.Configuration(i,this.getFields()).get(),r={},s=Object.keys(e),u=0;u0&&t.push(e);for(var i in n)"docs"!==i&&"df"!==i&&this.expandToken(e+i,t,n[i]);return t},t.InvertedIndex.prototype.toJSON=function(){return{root:this.root}},t.Configuration=function(e,n){var e=e||"";if(void 0==n||null==n)throw new Error("fields should not be null");this.config={};var i;try{i=JSON.parse(e),this.buildUserConfig(i,n)}catch(o){t.utils.warn("user configuration parse failed, will use default configuration"),this.buildDefaultConfig(n)}},t.Configuration.prototype.buildDefaultConfig=function(e){this.reset(),e.forEach(function(e){this.config[e]={boost:1,bool:"OR",expand:!1}},this)},t.Configuration.prototype.buildUserConfig=function(e,n){var i="OR",o=!1;if(this.reset(),"bool"in e&&(i=e.bool||i),"expand"in e&&(o=e.expand||o),"fields"in e)for(var r in e.fields)if(n.indexOf(r)>-1){var s=e.fields[r],u=o;void 0!=s.expand&&(u=s.expand),this.config[r]={boost:s.boost||0===s.boost?s.boost:1,bool:s.bool||i,expand:u}}else t.utils.warn("field name in user configuration not found in index instance fields");else this.addAllFields2UserConfig(i,o,n)},t.Configuration.prototype.addAllFields2UserConfig=function(e,t,n){n.forEach(function(n){this.config[n]={boost:1,bool:e,expand:t}},this)},t.Configuration.prototype.get=function(){return this.config},t.Configuration.prototype.reset=function(){this.config={}},lunr.SortedSet=function(){this.length=0,this.elements=[]},lunr.SortedSet.load=function(e){var t=new this;return t.elements=e,t.length=e.length,t},lunr.SortedSet.prototype.add=function(){var e,t;for(e=0;e1;){if(r===e)return o;e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o]}return r===e?o:-1},lunr.SortedSet.prototype.locationFor=function(e){for(var t=0,n=this.elements.length,i=n-t,o=t+Math.floor(i/2),r=this.elements[o];i>1;)e>r&&(t=o),r>e&&(n=o),i=n-t,o=t+Math.floor(i/2),r=this.elements[o];return r>e?o:e>r?o+1:void 0},lunr.SortedSet.prototype.intersect=function(e){for(var t=new lunr.SortedSet,n=0,i=0,o=this.length,r=e.length,s=this.elements,u=e.elements;;){if(n>o-1||i>r-1)break;s[n]!==u[i]?s[n]u[i]&&i++:(t.add(s[n]),n++,i++)}return t},lunr.SortedSet.prototype.clone=function(){var e=new lunr.SortedSet;return e.elements=this.toArray(),e.length=e.elements.length,e},lunr.SortedSet.prototype.union=function(e){var t,n,i;this.length>=e.length?(t=this,n=e):(t=e,n=this),i=t.clone();for(var o=0,r=n.toArray();oUnofficial TikTok API in Python

\n\n

This is an unofficial api wrapper for TikTok.com in python. With this api you are able to call most trending and fetch specific user information as well as much more.

\n\n

\"DOI\" \"LinkedIn\" \"Sponsor \"GitHub \"Build \"GitHub\" \"Downloads\" \"\" \"Support

\n\n

Sponsors

\n\n

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.

\n\n

\"TikAPI\" | TikAPI is a paid TikTok API service providing an full out-of-the-box solution for developers, trusted by 100+ companies. Learn more\n:-------------------------:|:-------------------------:

\n\n

Table of Contents

\n\n\n\n

Upgrading from V4 to V5

\n\n

Documentation

\n\n

You can find the full documentation here, the TikTokApi Class is where you'll probably spend most of your time.

\n\n

Getting Started

\n\n

To get started using this api follow the instructions below.

\n\n

How to Support The Project

\n\n
    \n
  • Star the repo \ud83d\ude0e
  • \n
  • Consider sponsoring me on GitHub
  • \n
  • Send me an email or a LinkedIn message telling me what you're using the API for, I really like hearing what people are using it for.
  • \n
  • Submit PRs for issues :)
  • \n
\n\n

Installing

\n\n

If you run into an issue please check the closed issues on the github, although feel free to re-open a new issue if you find an issue that's been closed for a few months. The codebase can and does run into similar issues as it has before, because TikTok changes things up.

\n\n
pip install TikTokApi\npython -m playwright install\n
\n\n

If you would prefer a video walk through of setting up this package I created a currently semi-outdated (TODO: new one for v5 coming soon) YouTube video just for that.

\n\n

Docker Installation

\n\n

Clone this repository onto a local machine (or just the Dockerfile since it installs TikTokApi from pip) then run the following commands.

\n\n
docker pull mcr.microsoft.com/playwright:focal\ndocker build . -t tiktokapi:latest\ndocker run -v TikTokApi --rm tiktokapi:latest python3 your_script.py\n
\n\n

Note this assumes your script is named your_script.py and lives in the root of this directory.

\n\n

Common Issues

\n\n

Please don't open an issue if you're experiencing one of these just comment if the provided solution do not work for you.

\n\n
    \n
  • Browser Has no Attribute - make sure you ran python3 -m playwright install, if your error persists try the playwright-python quickstart guide and diagnose issues from there.
  • \n
\n\n

Quick Start Guide

\n\n

Here's a quick bit of code to get the most recent trending videos on TikTok. There's more examples in the examples directory.

\n\n
from TikTokApi import TikTokApi\n\n# In your web browser you will need to go to TikTok, check the cookies \n# and under www.tiktok.com s_v_web_id should exist, and use that value\n# as input to custom_verify_fp\n# Or watch https://www.youtube.com/watch?v=zwLmLfVI-VQ for a visual\n# TODO: Update link\napi = TikTokApi(custom_verify_fp="")\n\nfor trending_video in api.trending.videos(count=50):\n    # Prints the author's username of the trending video.\n    print(trending_video.author.username)\n
\n\n

To run the example scripts from the repository root, make sure you use the -m option on python.

\n\n
python -m examples.get_trending\n
\n\n

You can access the dictionary type of an object using .as_dict. On a video this may look like\nthis, although TikTok changes their structure from time to time so it's worth investigating the structure of the dictionary when you use this package.

\n\n

Upgrading from V4 to V5

\n\n

All changes will be noted on #803 if you want more information.

\n\n

Motivation

\n\n

This package has been difficult to maintain due to it's structure, difficult to work with since the user of the package must write parsing methods to extract information from dictionaries, more memory intensive than it needs to be (although this can be further improved), and in general just difficult to work with for new users.

\n\n

As a result, I've decided to at least attempt to remedy some of these issues, the biggest changes are that

\n\n
    \n
  1. The package has shifted to using classes for different TikTok objects resulting in an easier, higher-level programming experience.
  2. \n
  3. All methods that used to return a list of objects have been switched to using generators, to hopefully decrease memory utilization for most users.
  4. \n
\n\n

Upgrading Examples

\n\n

Accessing Dictionary on Objects (similar to V4)

\n\n

TODO: Make video upgrading from V4-V5?

\n\n

You'll probably need to use this beyond just for legacy support, since not all attributes are parsed out and attached\nto the different objects.

\n\n

You may want to use this as a workaround for legacy applications while you upgrade the rest of the app. I'd suggest that you do eventually upgrade to using the higher-level approach fully.

\n\n
user = api.user(username='therock')\nuser.as_dict # -> dict of the user_object\nfor video in user.videos():\n    video.as_dict # -> dict of TikTok's video object as found when requesting the videos endpoint\n
\n\n

Here's a few more examples that help illustrate the differences in the flow of the usage of the package with V5.

\n\n
# V4\napi = TikTokApi.get_instance()\ntrending_videos = api.by_trending()\n\n#V5\napi = TikTokApi() # .get_instance no longer exists\nfor trending_video in api.trending.videos():\n    # do something\n
\n\n

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.

\n\n
# V4\ntrending_videos = api.by_trending()\nfor video in trending_videos:\n    # The dictionary responses are also different depending on what endpoint you got them from\n    # So, it's usually more painful than this to deal with\n    trending_user = api.get_user(id=video['author']['id'], secUid=video['author']['secUid'])\n\n\n# V5\n# This is more complicated than above, but it illustrates the simplified approach\nfor trending_video in api.trending.videos():\n    user_stats = trending_video.author.info_full['stats']\n    if user_stats['followerCount'] >= 10000:\n        # maybe save the user in a database\n
\n"}, {"fullname": "TikTokApi.api", "modulename": "TikTokApi.api", "type": "module", "doc": "

This module contains classes that all represent different types of data sent back by the TikTok servers.

\n\n

The files within in module correspond to what type of object is described and all have different methods associated with them.

\n"}, {"fullname": "TikTokApi.api.hashtag", "modulename": "TikTokApi.api.hashtag", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.hashtag.Hashtag", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag", "type": "class", "doc": "

A TikTok Hashtag/Challenge.

\n\n

Example Usage

\n\n
hashtag = api.hashtag(name='funny')\n
\n"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.__init__", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.__init__", "type": "function", "doc": "

You must provide the name or id of the hashtag.

\n", "signature": "(\n self,\n name: Optional[str] = None,\n id: Optional[str] = None,\n data: Optional[str] = None\n)", "funcdef": "def"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.id", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.id", "type": "variable", "doc": "

The ID of the hashtag

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.name", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.name", "type": "variable", "doc": "

The name of the hashtag (omiting the #)

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.as_dict", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.as_dict", "type": "variable", "doc": "

The raw data associated with this hashtag.

\n", "annotation": ": dict"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.info", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.info", "type": "function", "doc": "

Returns TikTok's dictionary representation of the hashtag object.

\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.info_full", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.info_full", "type": "function", "doc": "

Returns all information sent by TikTok related to this hashtag.

\n\n

Example Usage

\n\n
hashtag_data = api.hashtag(name='funny').info_full()\n
\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.videos", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.videos", "type": "function", "doc": "

Returns a dictionary listing TikToks with a specific hashtag.

\n\n
    \n
  • Parameters:\n
      \n
    • count (int): The amount of videos you want returned.
    • \n
    • cursor (int): The unix epoch to get videos since. TODO: Check this is right
    • \n
  • \n
\n\n

Example Usage

\n\n
for video in api.hashtag(name='funny').videos():\n    # do something\n
\n", "signature": "(\n self,\n count=30,\n offset=0,\n **kwargs\n) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]", "funcdef": "def"}, {"fullname": "TikTokApi.api.search", "modulename": "TikTokApi.api.search", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.search.Search", "modulename": "TikTokApi.api.search", "qualname": "Search", "type": "class", "doc": "

Contains static methods about searching.

\n"}, {"fullname": "TikTokApi.api.search.Search.__init__", "modulename": "TikTokApi.api.search", "qualname": "Search.__init__", "type": "function", "doc": "

\n", "signature": "()", "funcdef": "def"}, {"fullname": "TikTokApi.api.search.Search.users", "modulename": "TikTokApi.api.search", "qualname": "Search.users", "type": "function", "doc": "

Searches for users.

\n\n
    \n
  • Parameters:\n
      \n
    • search_term (str): The phrase you want to search for.
    • \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n\n

Example Usage

\n\n
for user in api.search.users('therock'):\n    # do something\n
\n", "signature": "(\n search_term,\n count=28,\n **kwargs\n) -> Generator[TikTokApi.api.user.User, NoneType, NoneType]", "funcdef": "def"}, {"fullname": "TikTokApi.api.search.Search.sounds", "modulename": "TikTokApi.api.search", "qualname": "Search.sounds", "type": "function", "doc": "

Searches for sounds.

\n\n
    \n
  • Parameters:\n
      \n
    • search_term (str): The phrase you want to search for.
    • \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n\n

Example Usage

\n\n
for user in api.search.sounds('funny'):\n    # do something\n
\n", "signature": "(\n search_term,\n count=28,\n **kwargs\n) -> Generator[TikTokApi.api.sound.Sound, NoneType, NoneType]", "funcdef": "def"}, {"fullname": "TikTokApi.api.search.Search.hashtags", "modulename": "TikTokApi.api.search", "qualname": "Search.hashtags", "type": "function", "doc": "

Searches for hashtags/challenges.

\n\n
    \n
  • Parameters:\n
      \n
    • search_term (str): The phrase you want to search for.
    • \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n\n

Example Usage

\n\n
for user in api.search.hashtags('funny'):\n    # do something\n
\n", "signature": "(\n search_term,\n count=28,\n **kwargs\n) -> Generator[TikTokApi.api.hashtag.Hashtag, NoneType, NoneType]", "funcdef": "def"}, {"fullname": "TikTokApi.api.search.Search.discover_type", "modulename": "TikTokApi.api.search", "qualname": "Search.discover_type", "type": "function", "doc": "

Searches for a specific type of object.\nYou should instead use the users/sounds/hashtags as they all use data\nfrom this function.

\n\n
    \n
  • Parameters:\n
      \n
    • search_term (str): The phrase you want to search for.
    • \n
    • prefix (str): either user|music|challenge
    • \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n\n

Example Usage

\n\n
for user in api.search.discover_type('therock', 'user'):\n    # do something\n
\n", "signature": "(search_term, prefix, count=28, offset=0, **kwargs) -> list", "funcdef": "def"}, {"fullname": "TikTokApi.api.search.Search.users_alternate", "modulename": "TikTokApi.api.search", "qualname": "Search.users_alternate", "type": "function", "doc": "

Searches for users using an alternate endpoint than Search.users

\n\n
    \n
  • Parameters:\n
      \n
    • search_term (str): The phrase you want to search for.
    • \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n\n

Example Usage

\n\n
for user in api.search.users_alternate('therock'):\n    # do something\n
\n", "signature": "(search_term, count=28, offset=0, **kwargs) -> list", "funcdef": "def"}, {"fullname": "TikTokApi.api.sound", "modulename": "TikTokApi.api.sound", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.sound.Sound", "modulename": "TikTokApi.api.sound", "qualname": "Sound", "type": "class", "doc": "

A TikTok Sound/Music/Song.

\n\n

Example Usage

\n\n
song = api.song(id='7016547803243022337')\n
\n"}, {"fullname": "TikTokApi.api.sound.Sound.__init__", "modulename": "TikTokApi.api.sound", "qualname": "Sound.__init__", "type": "function", "doc": "

You must provide the id of the sound or it will not work.

\n", "signature": "(self, id: Optional[str] = None, data: Optional[str] = None)", "funcdef": "def"}, {"fullname": "TikTokApi.api.sound.Sound.id", "modulename": "TikTokApi.api.sound", "qualname": "Sound.id", "type": "variable", "doc": "

TikTok's ID for the sound

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.sound.Sound.title", "modulename": "TikTokApi.api.sound", "qualname": "Sound.title", "type": "variable", "doc": "

The title of the song.

\n", "annotation": ": Optional[str]"}, {"fullname": "TikTokApi.api.sound.Sound.author", "modulename": "TikTokApi.api.sound", "qualname": "Sound.author", "type": "variable", "doc": "

The author of the song (if it exists)

\n", "annotation": ": Optional[TikTokApi.api.user.User]"}, {"fullname": "TikTokApi.api.sound.Sound.info", "modulename": "TikTokApi.api.sound", "qualname": "Sound.info", "type": "function", "doc": "

Returns a dictionary of TikTok's Sound/Music object.

\n\n
    \n
  • Parameters:\n
      \n
    • use_html (bool): If you want to perform an HTML request or not.\nDefaults to False to use an API call, which shouldn't get detected\nas often as an HTML request.
    • \n
  • \n
\n\n

Example Usage

\n\n
sound_data = api.sound(id='7016547803243022337').info()\n
\n", "signature": "(self, use_html=False, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.sound.Sound.info_full", "modulename": "TikTokApi.api.sound", "qualname": "Sound.info_full", "type": "function", "doc": "

Returns all the data associated with a TikTok Sound.

\n\n

This makes an API request, there is no HTML request option, as such\nwith Sound.info()

\n\n

Example Usage

\n\n
sound_data = api.sound(id='7016547803243022337').info_full()\n
\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.sound.Sound.videos", "modulename": "TikTokApi.api.sound", "qualname": "Sound.videos", "type": "function", "doc": "

Returns Video objects of videos created with this sound.

\n\n
    \n
  • Parameters:\n
      \n
    • count (int): The amount of videos you want returned.
    • \n
    • cursor (int): The unix epoch to get videos since. TODO: Check this is right
    • \n
  • \n
\n\n

Example Usage

\n\n
for video in api.sound(id='7016547803243022337').videos():\n    # do something\n
\n", "signature": "(\n self,\n count=30,\n offset=0,\n **kwargs\n) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]", "funcdef": "def"}, {"fullname": "TikTokApi.api.trending", "modulename": "TikTokApi.api.trending", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.trending.Trending", "modulename": "TikTokApi.api.trending", "qualname": "Trending", "type": "class", "doc": "

Contains static methods related to trending.

\n"}, {"fullname": "TikTokApi.api.trending.Trending.__init__", "modulename": "TikTokApi.api.trending", "qualname": "Trending.__init__", "type": "function", "doc": "

\n", "signature": "()", "funcdef": "def"}, {"fullname": "TikTokApi.api.trending.Trending.videos", "modulename": "TikTokApi.api.trending", "qualname": "Trending.videos", "type": "function", "doc": "

Returns Videos that are trending on TikTok.

\n\n
    \n
  • Parameters:\n
      \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n", "signature": "(\n count=30,\n **kwargs\n) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]", "funcdef": "def"}, {"fullname": "TikTokApi.api.user", "modulename": "TikTokApi.api.user", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.user.User", "modulename": "TikTokApi.api.user", "qualname": "User", "type": "class", "doc": "

A TikTok User.

\n\n

Example Usage

\n\n
user = api.user(username='therock')\n# or\nuser_id = '5831967'\nsec_uid = 'MS4wLjABAAAA-VASjiXTh7wDDyXvjk10VFhMWUAoxr8bgfO1kAL1-9s'\nuser = api.user(user_id=user_id, sec_uid=sec_uid)\n
\n"}, {"fullname": "TikTokApi.api.user.User.__init__", "modulename": "TikTokApi.api.user", "qualname": "User.__init__", "type": "function", "doc": "

You must provide the username or (user_id and sec_uid) otherwise this\nwill not function correctly.

\n", "signature": "(\n self,\n username: Optional[str] = None,\n user_id: Optional[str] = None,\n sec_uid: Optional[str] = None,\n data: Optional[str] = None\n)", "funcdef": "def"}, {"fullname": "TikTokApi.api.user.User.user_id", "modulename": "TikTokApi.api.user", "qualname": "User.user_id", "type": "variable", "doc": "

The user ID of the user.

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.user.User.sec_uid", "modulename": "TikTokApi.api.user", "qualname": "User.sec_uid", "type": "variable", "doc": "

The sec UID of the user.

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.user.User.username", "modulename": "TikTokApi.api.user", "qualname": "User.username", "type": "variable", "doc": "

The username of the user.

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.user.User.as_dict", "modulename": "TikTokApi.api.user", "qualname": "User.as_dict", "type": "variable", "doc": "

The raw data associated with this user.

\n", "annotation": ": dict"}, {"fullname": "TikTokApi.api.user.User.info", "modulename": "TikTokApi.api.user", "qualname": "User.info", "type": "function", "doc": "

Returns a dictionary of TikTok's User object

\n\n

Example Usage

\n\n
user_data = api.user(username='therock').info()\n
\n", "signature": "(self, **kwargs)", "funcdef": "def"}, {"fullname": "TikTokApi.api.user.User.info_full", "modulename": "TikTokApi.api.user", "qualname": "User.info_full", "type": "function", "doc": "

Returns a dictionary of information associated with this User.\nIncludes statistics about this user.

\n\n

Example Usage

\n\n
user_data = api.user(username='therock').info_full()\n
\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.user.User.videos", "modulename": "TikTokApi.api.user", "qualname": "User.videos", "type": "function", "doc": "

Returns a Generator yielding Video objects.

\n\n
    \n
  • Parameters:\n
      \n
    • count (int): The amount of videos you want returned.
    • \n
    • cursor (int): The unix epoch to get videos since. TODO: Check this is right
    • \n
  • \n
\n\n

Example Usage

\n\n
user = api.user(username='therock')\nfor video in user.videos(count=100):\n    print(video.id)\n
\n", "signature": "(\n self,\n count=30,\n cursor=0,\n **kwargs\n) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]", "funcdef": "def"}, {"fullname": "TikTokApi.api.user.User.liked", "modulename": "TikTokApi.api.user", "qualname": "User.liked", "type": "function", "doc": "

Returns a dictionary listing TikToks that a given a user has liked.

\n\n

Note: The user's likes must be public (which is not the default option)

\n\n
    \n
  • Parameters:\n
      \n
    • count (int): The amount of videos you want returned.
    • \n
    • cursor (int): The unix epoch to get videos since. TODO: Check this is right
    • \n
  • \n
\n\n

Example Usage

\n\n
for liked_video in api.user(username='public_likes'):\n    print(liked_video.id)\n
\n", "signature": "(\n self,\n count: int = 30,\n cursor: int = 0,\n **kwargs\n) -> Generator[TikTokApi.api.video.Video, NoneType, NoneType]", "funcdef": "def"}, {"fullname": "TikTokApi.api.video", "modulename": "TikTokApi.api.video", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.video.Video", "modulename": "TikTokApi.api.video", "qualname": "Video", "type": "class", "doc": "

A TikTok Video class

\n\n

Example Usage

\n\n
video = api.video(id='7041997751718137094')\n
\n"}, {"fullname": "TikTokApi.api.video.Video.__init__", "modulename": "TikTokApi.api.video", "qualname": "Video.__init__", "type": "function", "doc": "

You must provide the id or a valid url, else this will fail.

\n", "signature": "(\n self,\n id: Optional[str] = None,\n url: Optional[str] = None,\n data: Optional[dict] = None\n)", "funcdef": "def"}, {"fullname": "TikTokApi.api.video.Video.id", "modulename": "TikTokApi.api.video", "qualname": "Video.id", "type": "variable", "doc": "

TikTok's ID of the Video

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.video.Video.author", "modulename": "TikTokApi.api.video", "qualname": "Video.author", "type": "variable", "doc": "

The User who created the Video

\n", "annotation": ": Optional[TikTokApi.api.user.User]"}, {"fullname": "TikTokApi.api.video.Video.sound", "modulename": "TikTokApi.api.video", "qualname": "Video.sound", "type": "variable", "doc": "

The Sound that is associated with the Video

\n", "annotation": ": Optional[TikTokApi.api.sound.Sound]"}, {"fullname": "TikTokApi.api.video.Video.hashtags", "modulename": "TikTokApi.api.video", "qualname": "Video.hashtags", "type": "variable", "doc": "

A List of Hashtags on the Video

\n", "annotation": ": Optional[list[TikTokApi.api.hashtag.Hashtag]]"}, {"fullname": "TikTokApi.api.video.Video.as_dict", "modulename": "TikTokApi.api.video", "qualname": "Video.as_dict", "type": "variable", "doc": "

The raw data associated with this Video.

\n", "annotation": ": dict"}, {"fullname": "TikTokApi.api.video.Video.info", "modulename": "TikTokApi.api.video", "qualname": "Video.info", "type": "function", "doc": "

Returns a dictionary of TikTok's Video object.

\n\n

Example Usage

\n\n
video_data = api.video(id='7041997751718137094').info()\n
\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.video.Video.info_full", "modulename": "TikTokApi.api.video", "qualname": "Video.info_full", "type": "function", "doc": "

Returns a dictionary of all data associated with a TikTok Video.

\n\n

Example Usage

\n\n
video_data = api.video(id='7041997751718137094').info_full()\n
\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.video.Video.bytes", "modulename": "TikTokApi.api.video", "qualname": "Video.bytes", "type": "function", "doc": "

Returns the bytes of a TikTok Video.

\n\n

Example Usage

\n\n
video_bytes = api.video(id='7041997751718137094').bytes()\n\n# Saving The Video\nwith open('saved_video.mp4', 'wb') as output:\n    output.write(video_bytes)\n
\n", "signature": "(self, **kwargs) -> bytes", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities", "modulename": "TikTokApi.browser_utilities", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.browser_utilities.browser", "modulename": "TikTokApi.browser_utilities.browser", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.browser_utilities.browser.get_playwright", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "get_playwright", "type": "function", "doc": "

\n", "signature": "()", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser.browser", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser", "type": "class", "doc": "

Helper class that provides a standard way to create an ABC using\ninheritance.

\n", "bases": "TikTokApi.browser_utilities.browser_interface.BrowserInterface"}, {"fullname": "TikTokApi.browser_utilities.browser.browser.__init__", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.__init__", "type": "function", "doc": "

\n", "signature": "(self, **kwargs)", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser.browser.get_params", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.get_params", "type": "function", "doc": "

\n", "signature": "(self, page) -> None", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser.browser.gen_verifyFp", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.gen_verifyFp", "type": "function", "doc": "

\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser.browser.sign_url", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.sign_url", "type": "function", "doc": "

\n", "signature": "(self, url, calc_tt_params=False, **kwargs)", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser.browser.find_redirect", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.find_redirect", "type": "function", "doc": "

\n", "signature": "(self, url)", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser_interface", "modulename": "TikTokApi.browser_utilities.browser_interface", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.browser_utilities.browser_interface.BrowserInterface", "modulename": "TikTokApi.browser_utilities.browser_interface", "qualname": "BrowserInterface", "type": "class", "doc": "

Helper class that provides a standard way to create an ABC using\ninheritance.

\n", "bases": "abc.ABC"}, {"fullname": "TikTokApi.browser_utilities.browser_interface.BrowserInterface.get_params", "modulename": "TikTokApi.browser_utilities.browser_interface", "qualname": "BrowserInterface.get_params", "type": "function", "doc": "

\n", "signature": "(self, page) -> None", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser_interface.BrowserInterface.sign_url", "modulename": "TikTokApi.browser_utilities.browser_interface", "qualname": "BrowserInterface.sign_url", "type": "function", "doc": "

\n", "signature": "(self, calc_tt_params=False, **kwargs)", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.get_acrawler", "modulename": "TikTokApi.browser_utilities.get_acrawler", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.exceptions", "modulename": "TikTokApi.exceptions", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.exceptions.TikTokCaptchaError", "modulename": "TikTokApi.exceptions", "qualname": "TikTokCaptchaError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.TikTokCaptchaError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "TikTokCaptchaError.__init__", "type": "function", "doc": "

\n", "signature": "(\n self,\n message='TikTok blocks this request displaying a Captcha \\nTip: Consider using a proxy or a custom_verify_fp as method parameters'\n)", "funcdef": "def"}, {"fullname": "TikTokApi.exceptions.GenericTikTokError", "modulename": "TikTokApi.exceptions", "qualname": "GenericTikTokError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.GenericTikTokError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "GenericTikTokError.__init__", "type": "function", "doc": "

\n", "signature": "(self, message)", "funcdef": "def"}, {"fullname": "TikTokApi.exceptions.TikTokNotFoundError", "modulename": "TikTokApi.exceptions", "qualname": "TikTokNotFoundError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.TikTokNotFoundError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "TikTokNotFoundError.__init__", "type": "function", "doc": "

\n", "signature": "(self, message='The requested object does not exists')", "funcdef": "def"}, {"fullname": "TikTokApi.exceptions.EmptyResponseError", "modulename": "TikTokApi.exceptions", "qualname": "EmptyResponseError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.EmptyResponseError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "EmptyResponseError.__init__", "type": "function", "doc": "

\n", "signature": "(self, message='TikTok sent no data back')", "funcdef": "def"}, {"fullname": "TikTokApi.exceptions.JSONDecodeFailure", "modulename": "TikTokApi.exceptions", "qualname": "JSONDecodeFailure", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.JSONDecodeFailure.__init__", "modulename": "TikTokApi.exceptions", "qualname": "JSONDecodeFailure.__init__", "type": "function", "doc": "

\n", "signature": "(self, message='TikTok sent invalid JSON back')", "funcdef": "def"}, {"fullname": "TikTokApi.exceptions.TikTokNotAvailableError", "modulename": "TikTokApi.exceptions", "qualname": "TikTokNotAvailableError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.TikTokNotAvailableError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "TikTokNotAvailableError.__init__", "type": "function", "doc": "

\n", "signature": "(self, message='The requested object is not available in this region')", "funcdef": "def"}, {"fullname": "TikTokApi.helpers", "modulename": "TikTokApi.helpers", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.helpers.extract_tag_contents", "modulename": "TikTokApi.helpers", "qualname": "extract_tag_contents", "type": "function", "doc": "

\n", "signature": "(html)", "funcdef": "def"}, {"fullname": "TikTokApi.helpers.extract_video_id_from_url", "modulename": "TikTokApi.helpers", "qualname": "extract_video_id_from_url", "type": "function", "doc": "

\n", "signature": "(url)", "funcdef": "def"}, {"fullname": "TikTokApi.tiktok", "modulename": "TikTokApi.tiktok", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi", "type": "class", "doc": "

\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.__init__", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.__init__", "type": "function", "doc": "

The TikTokApi class. Used to interact with TikTok. This is a singleton\n class to prevent issues from arising with playwright

\n\n
Parameters
\n\n
    \n
  • logging_level: The logging level you want the program to run at, optional\nThese are the standard python logging module's levels.

  • \n
  • request_delay: The amount of time in seconds to wait before making a request, optional\nThis is used to throttle your own requests as you may end up making too\nmany requests to TikTok for your IP.

  • \n
  • custom_device_id: A TikTok parameter needed to download videos, optional\nThe code generates these and handles these pretty well itself, however\nfor some things such as video download you will need to set a consistent\none of these. All the methods take this as a optional parameter, however\nit's cleaner code to store this at the instance level. You can override\nthis at the specific methods.

  • \n
  • generate_static_device_id: A parameter that generates a custom_device_id at the instance level\nUse this if you want to download videos from a script but don't want to generate\nyour own custom_device_id parameter.

  • \n
  • custom_verify_fp: A TikTok parameter needed to work most of the time, optional\nTo get this parameter look at this video\nI recommend watching the entire thing, as it will help setup this package. All\nthe methods take this as a optional parameter, however it's cleaner code\nto store this at the instance level. You can override this at the specific\nmethods.

    \n\n

    You can use the following to generate \"\".join(random.choice(string.digits)\nfor num in range(19))

  • \n
  • use_test_endpoints: Send requests to TikTok's test endpoints, optional\nThis parameter when set to true will make requests to TikTok's testing\nendpoints instead of the live site. I can't guarantee this will work\nin the future, however currently basically any custom_verify_fp will\nwork here which is helpful.

  • \n
  • proxy: A string containing your proxy address, optional\nIf you want to do a lot of scraping of TikTok endpoints you'll likely\nneed a proxy.

    \n\n

    Ex: \"https://0.0.0.0:8080\"

    \n\n

    All the methods take this as a optional parameter, however it's cleaner code\nto store this at the instance level. You can override this at the specific\nmethods.

  • \n
  • use_selenium: Option to use selenium over playwright, optional\nPlaywright is selected by default and is the one that I'm designing the\npackage to be compatable for, however if playwright doesn't work on\nyour machine feel free to set this to True.

  • \n
  • executablePath: The location of the driver, optional\nThis shouldn't be needed if you're using playwright

  • \n
  • **kwargs\nParameters that are passed on to basically every module and methods\nthat interact with this main class. These may or may not be documented\nin other places.

  • \n
\n", "signature": "(cls, logging_level=30, *args, **kwargs)", "funcdef": "def"}, {"fullname": "TikTokApi.tiktok.TikTokApi.logger", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.logger", "type": "variable", "doc": "

\n", "annotation": ": ClassVar[logging.Logger]", "default_value": " = "}, {"fullname": "TikTokApi.tiktok.TikTokApi.user", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.user", "type": "class", "doc": "

A TikTok User.

\n\n

Example Usage

\n\n
user = api.user(username='therock')\n# or\nuser_id = '5831967'\nsec_uid = 'MS4wLjABAAAA-VASjiXTh7wDDyXvjk10VFhMWUAoxr8bgfO1kAL1-9s'\nuser = api.user(user_id=user_id, sec_uid=sec_uid)\n
\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.search", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.search", "type": "class", "doc": "

Contains static methods about searching.

\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.sound", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.sound", "type": "class", "doc": "

A TikTok Sound/Music/Song.

\n\n

Example Usage

\n\n
song = api.song(id='7016547803243022337')\n
\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.hashtag", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.hashtag", "type": "class", "doc": "

A TikTok Hashtag/Challenge.

\n\n

Example Usage

\n\n
hashtag = api.hashtag(name='funny')\n
\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.video", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.video", "type": "class", "doc": "

A TikTok Video class

\n\n

Example Usage

\n\n
video = api.video(id='7041997751718137094')\n
\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.trending", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.trending", "type": "class", "doc": "

Contains static methods related to trending.

\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.get_data", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_data", "type": "function", "doc": "

Makes requests to TikTok and returns their JSON.

\n\n

This is all handled by the package so it's unlikely\nyou will need to use this.

\n", "signature": "(self, path, use_desktop_base_url=False, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.tiktok.TikTokApi.external_signer", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.external_signer", "type": "function", "doc": "

Makes requests to an external signer instead of using a browser.

\n\n
Parameters
\n\n
    \n
  • url: The server to make requests to\nThis server is designed to sign requests. You can find an example\nof this signature server in the examples folder.

  • \n
  • custom_device_id: A TikTok parameter needed to download videos\nThe code generates these and handles these pretty well itself, however\nfor some things such as video download you will need to set a consistent\none of these.

  • \n
  • custom_verify_fp: A TikTok parameter needed to work most of the time,\nTo get this parameter look at this video\nI recommend watching the entire thing, as it will help setup this package.

  • \n
\n", "signature": "(self, url, custom_device_id=None, verifyFp=None)", "funcdef": "def"}, {"fullname": "TikTokApi.tiktok.TikTokApi.get_bytes", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_bytes", "type": "function", "doc": "

Returns TikTok's response as bytes, similar to get_data

\n", "signature": "(self, **kwargs) -> bytes", "funcdef": "def"}, {"fullname": "TikTokApi.tiktok.TikTokApi.generate_device_id", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.generate_device_id", "type": "function", "doc": "

Generates a valid device_id for other methods. Pass this as the custom_device_id field to download videos

\n", "signature": "()", "funcdef": "def"}, {"fullname": "TikTokApi.utilities", "modulename": "TikTokApi.utilities", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.utilities.LOGGER_NAME", "modulename": "TikTokApi.utilities", "qualname": "LOGGER_NAME", "type": "variable", "doc": "

\n", "annotation": ": str", "default_value": " = 'TikTokApi'"}, {"fullname": "TikTokApi.utilities.update_messager", "modulename": "TikTokApi.utilities", "qualname": "update_messager", "type": "function", "doc": "

\n", "signature": "()", "funcdef": "def"}, {"fullname": "TikTokApi.utilities.check", "modulename": "TikTokApi.utilities", "qualname": "check", "type": "function", "doc": "

\n", "signature": "(name)", "funcdef": "def"}, {"fullname": "TikTokApi.utilities.check_future_deprecation", "modulename": "TikTokApi.utilities", "qualname": "check_future_deprecation", "type": "function", "doc": "

\n", "signature": "()", "funcdef": "def"}]; + /** pdoc search index */const docs = [{"fullname": "TikTokApi", "modulename": "TikTokApi", "type": "module", "doc": "

Unofficial TikTok API in Python

\n\n

This is an unofficial api wrapper for TikTok.com in python. With this api you are able to call most trending and fetch specific user information as well as much more.

\n\n

\"DOI\" \"LinkedIn\" \"Sponsor \"GitHub \"Build \"GitHub\" \"Downloads\" \"\" \"Support

\n\n

Sponsors

\n\n

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.

\n\n

\"TikAPI\" | TikAPI is a paid TikTok API service providing an full out-of-the-box solution for developers, trusted by 100+ companies. Learn more\n:-------------------------:|:-------------------------:

\n\n

Table of Contents

\n\n\n\n

Upgrading from V4 to V5

\n\n

Documentation

\n\n

You can find the full documentation here, the TikTokApi Class is where you'll probably spend most of your time.

\n\n

Getting Started

\n\n

To get started using this api follow the instructions below.

\n\n

How to Support The Project

\n\n
    \n
  • Star the repo \ud83d\ude0e
  • \n
  • Consider sponsoring me on GitHub
  • \n
  • Send me an email or a LinkedIn message telling me what you're using the API for, I really like hearing what people are using it for.
  • \n
  • Submit PRs for issues :)
  • \n
\n\n

Installing

\n\n

If you run into an issue please check the closed issues on the github, although feel free to re-open a new issue if you find an issue that's been closed for a few months. The codebase can and does run into similar issues as it has before, because TikTok changes things up.

\n\n
pip install TikTokApi\npython -m playwright install\n
\n\n

If you would prefer a video walk through of setting up this package I created a currently semi-outdated (TODO: new one for v5 coming soon) YouTube video just for that.

\n\n

Docker Installation

\n\n

Clone this repository onto a local machine (or just the Dockerfile since it installs TikTokApi from pip) then run the following commands.

\n\n
docker pull mcr.microsoft.com/playwright:focal\ndocker build . -t tiktokapi:latest\ndocker run -v TikTokApi --rm tiktokapi:latest python3 your_script.py\n
\n\n

Note this assumes your script is named your_script.py and lives in the root of this directory.

\n\n

Common Issues

\n\n

Please don't open an issue if you're experiencing one of these just comment if the provided solution do not work for you.

\n\n
    \n
  • Browser Has no Attribute - make sure you ran python3 -m playwright install, if your error persists try the playwright-python quickstart guide and diagnose issues from there.
  • \n
\n\n

Quick Start Guide

\n\n

Here's a quick bit of code to get the most recent trending videos on TikTok. There's more examples in the examples directory.

\n\n
from TikTokApi import TikTokApi\n\n# In your web browser you will need to go to TikTok, check the cookies \n# and under www.tiktok.com s_v_web_id should exist, and use that value\n# as input to custom_verify_fp\n# Or watch https://www.youtube.com/watch?v=zwLmLfVI-VQ for a visual\n# TODO: Update link\napi = TikTokApi(custom_verify_fp="")\n\nfor trending_video in api.trending.videos(count=50):\n    # Prints the author's username of the trending video.\n    print(trending_video.author.username)\n
\n\n

To run the example scripts from the repository root, make sure you use the -m option on python.

\n\n
python -m examples.get_trending\n
\n\n

You can access the dictionary type of an object using .as_dict. On a video this may look like\nthis, although TikTok changes their structure from time to time so it's worth investigating the structure of the dictionary when you use this package.

\n\n

Upgrading from V4 to V5

\n\n

All changes will be noted on #803 if you want more information.

\n\n

Motivation

\n\n

This package has been difficult to maintain due to it's structure, difficult to work with since the user of the package must write parsing methods to extract information from dictionaries, more memory intensive than it needs to be (although this can be further improved), and in general just difficult to work with for new users.

\n\n

As a result, I've decided to at least attempt to remedy some of these issues, the biggest changes are that

\n\n
    \n
  1. The package has shifted to using classes for different TikTok objects resulting in an easier, higher-level programming experience.
  2. \n
  3. All methods that used to return a list of objects have been switched to using iterators, to hopefully decrease memory utilization for most users.
  4. \n
\n\n

Upgrading Examples

\n\n

Accessing Dictionary on Objects (similar to V4)

\n\n

TODO: Make video upgrading from V4-V5?

\n\n

You'll probably need to use this beyond just for legacy support, since not all attributes are parsed out and attached\nto the different objects.

\n\n

You may want to use this as a workaround for legacy applications while you upgrade the rest of the app. I'd suggest that you do eventually upgrade to using the higher-level approach fully.

\n\n
user = api.user(username='therock')\nuser.as_dict # -> dict of the user_object\nfor video in user.videos():\n    video.as_dict # -> dict of TikTok's video object as found when requesting the videos endpoint\n
\n\n

Here's a few more examples that help illustrate the differences in the flow of the usage of the package with V5.

\n\n
# V4\napi = TikTokApi.get_instance()\ntrending_videos = api.by_trending()\n\n#V5\napi = TikTokApi() # .get_instance no longer exists\nfor trending_video in api.trending.videos():\n    # do something\n
\n\n

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.

\n\n
# V4\ntrending_videos = api.by_trending()\nfor video in trending_videos:\n    # The dictionary responses are also different depending on what endpoint you got them from\n    # So, it's usually more painful than this to deal with\n    trending_user = api.get_user(id=video['author']['id'], secUid=video['author']['secUid'])\n\n\n# V5\n# This is more complicated than above, but it illustrates the simplified approach\nfor trending_video in api.trending.videos():\n    user_stats = trending_video.author.info_full['stats']\n    if user_stats['followerCount'] >= 10000:\n        # maybe save the user in a database\n
\n"}, {"fullname": "TikTokApi.api", "modulename": "TikTokApi.api", "type": "module", "doc": "

This module contains classes that all represent different types of data sent back by the TikTok servers.

\n\n

The files within in module correspond to what type of object is described and all have different methods associated with them.

\n"}, {"fullname": "TikTokApi.api.hashtag", "modulename": "TikTokApi.api.hashtag", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.hashtag.Hashtag", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag", "type": "class", "doc": "

A TikTok Hashtag/Challenge.

\n\n

Example Usage

\n\n
hashtag = api.hashtag(name='funny')\n
\n"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.__init__", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.__init__", "type": "function", "doc": "

You must provide the name or id of the hashtag.

\n", "signature": "(\n self,\n name: Optional[str] = None,\n id: Optional[str] = None,\n data: Optional[dict] = None\n)", "funcdef": "def"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.id", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.id", "type": "variable", "doc": "

The ID of the hashtag

\n", "annotation": ": Optional[str]"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.name", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.name", "type": "variable", "doc": "

The name of the hashtag (omiting the #)

\n", "annotation": ": Optional[str]"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.as_dict", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.as_dict", "type": "variable", "doc": "

The raw data associated with this hashtag.

\n", "annotation": ": dict"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.info", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.info", "type": "function", "doc": "

Returns TikTok's dictionary representation of the hashtag object.

\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.info_full", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.info_full", "type": "function", "doc": "

Returns all information sent by TikTok related to this hashtag.

\n\n

Example Usage

\n\n
hashtag_data = api.hashtag(name='funny').info_full()\n
\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.hashtag.Hashtag.videos", "modulename": "TikTokApi.api.hashtag", "qualname": "Hashtag.videos", "type": "function", "doc": "

Returns a dictionary listing TikToks with a specific hashtag.

\n\n
    \n
  • Parameters:\n
      \n
    • count (int): The amount of videos you want returned.
    • \n
    • cursor (int): The unix epoch to get videos since. TODO: Check this is right
    • \n
  • \n
\n\n

Example Usage

\n\n
for video in api.hashtag(name='funny').videos():\n    # do something\n
\n", "signature": "(\n self,\n count=30,\n offset=0,\n **kwargs\n) -> Iterator[TikTokApi.api.video.Video]", "funcdef": "def"}, {"fullname": "TikTokApi.api.search", "modulename": "TikTokApi.api.search", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.search.Search", "modulename": "TikTokApi.api.search", "qualname": "Search", "type": "class", "doc": "

Contains static methods about searching.

\n"}, {"fullname": "TikTokApi.api.search.Search.__init__", "modulename": "TikTokApi.api.search", "qualname": "Search.__init__", "type": "function", "doc": "

\n", "signature": "()", "funcdef": "def"}, {"fullname": "TikTokApi.api.search.Search.users", "modulename": "TikTokApi.api.search", "qualname": "Search.users", "type": "function", "doc": "

Searches for users.

\n\n
    \n
  • Parameters:\n
      \n
    • search_term (str): The phrase you want to search for.
    • \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n\n

Example Usage

\n\n
for user in api.search.users('therock'):\n    # do something\n
\n", "signature": "(search_term, count=28, **kwargs) -> Iterator[TikTokApi.api.user.User]", "funcdef": "def"}, {"fullname": "TikTokApi.api.search.Search.sounds", "modulename": "TikTokApi.api.search", "qualname": "Search.sounds", "type": "function", "doc": "

Searches for sounds.

\n\n
    \n
  • Parameters:\n
      \n
    • search_term (str): The phrase you want to search for.
    • \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n\n

Example Usage

\n\n
for user in api.search.sounds('funny'):\n    # do something\n
\n", "signature": "(\n search_term,\n count=28,\n **kwargs\n) -> Iterator[TikTokApi.api.sound.Sound]", "funcdef": "def"}, {"fullname": "TikTokApi.api.search.Search.hashtags", "modulename": "TikTokApi.api.search", "qualname": "Search.hashtags", "type": "function", "doc": "

Searches for hashtags/challenges.

\n\n
    \n
  • Parameters:\n
      \n
    • search_term (str): The phrase you want to search for.
    • \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n\n

Example Usage

\n\n
for user in api.search.hashtags('funny'):\n    # do something\n
\n", "signature": "(\n search_term,\n count=28,\n **kwargs\n) -> Iterator[TikTokApi.api.hashtag.Hashtag]", "funcdef": "def"}, {"fullname": "TikTokApi.api.search.Search.discover_type", "modulename": "TikTokApi.api.search", "qualname": "Search.discover_type", "type": "function", "doc": "

Searches for a specific type of object.\nYou should instead use the users/sounds/hashtags as they all use data\nfrom this function.

\n\n
    \n
  • Parameters:\n
      \n
    • search_term (str): The phrase you want to search for.
    • \n
    • prefix (str): either user|music|challenge
    • \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n\n

Example Usage

\n\n
for user in api.search.discover_type('therock', 'user'):\n    # do something\n
\n", "signature": "(search_term, prefix, count=28, offset=0, **kwargs) -> Iterator", "funcdef": "def"}, {"fullname": "TikTokApi.api.search.Search.users_alternate", "modulename": "TikTokApi.api.search", "qualname": "Search.users_alternate", "type": "function", "doc": "

Searches for users using an alternate endpoint than Search.users

\n\n
    \n
  • Parameters:\n
      \n
    • search_term (str): The phrase you want to search for.
    • \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n\n

Example Usage

\n\n
for user in api.search.users_alternate('therock'):\n    # do something\n
\n", "signature": "(\n search_term,\n count=28,\n offset=0,\n **kwargs\n) -> Iterator[TikTokApi.api.user.User]", "funcdef": "def"}, {"fullname": "TikTokApi.api.sound", "modulename": "TikTokApi.api.sound", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.sound.Sound", "modulename": "TikTokApi.api.sound", "qualname": "Sound", "type": "class", "doc": "

A TikTok Sound/Music/Song.

\n\n

Example Usage

\n\n
song = api.song(id='7016547803243022337')\n
\n"}, {"fullname": "TikTokApi.api.sound.Sound.__init__", "modulename": "TikTokApi.api.sound", "qualname": "Sound.__init__", "type": "function", "doc": "

You must provide the id of the sound or it will not work.

\n", "signature": "(self, id: Optional[str] = None, data: Optional[str] = None)", "funcdef": "def"}, {"fullname": "TikTokApi.api.sound.Sound.id", "modulename": "TikTokApi.api.sound", "qualname": "Sound.id", "type": "variable", "doc": "

TikTok's ID for the sound

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.sound.Sound.title", "modulename": "TikTokApi.api.sound", "qualname": "Sound.title", "type": "variable", "doc": "

The title of the song.

\n", "annotation": ": Optional[str]"}, {"fullname": "TikTokApi.api.sound.Sound.author", "modulename": "TikTokApi.api.sound", "qualname": "Sound.author", "type": "variable", "doc": "

The author of the song (if it exists)

\n", "annotation": ": Optional[TikTokApi.api.user.User]"}, {"fullname": "TikTokApi.api.sound.Sound.info", "modulename": "TikTokApi.api.sound", "qualname": "Sound.info", "type": "function", "doc": "

Returns a dictionary of TikTok's Sound/Music object.

\n\n
    \n
  • Parameters:\n
      \n
    • use_html (bool): If you want to perform an HTML request or not.\nDefaults to False to use an API call, which shouldn't get detected\nas often as an HTML request.
    • \n
  • \n
\n\n

Example Usage

\n\n
sound_data = api.sound(id='7016547803243022337').info()\n
\n", "signature": "(self, use_html=False, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.sound.Sound.info_full", "modulename": "TikTokApi.api.sound", "qualname": "Sound.info_full", "type": "function", "doc": "

Returns all the data associated with a TikTok Sound.

\n\n

This makes an API request, there is no HTML request option, as such\nwith Sound.info()

\n\n

Example Usage

\n\n
sound_data = api.sound(id='7016547803243022337').info_full()\n
\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.sound.Sound.videos", "modulename": "TikTokApi.api.sound", "qualname": "Sound.videos", "type": "function", "doc": "

Returns Video objects of videos created with this sound.

\n\n
    \n
  • Parameters:\n
      \n
    • count (int): The amount of videos you want returned.
    • \n
    • cursor (int): The unix epoch to get videos since. TODO: Check this is right
    • \n
  • \n
\n\n

Example Usage

\n\n
for video in api.sound(id='7016547803243022337').videos():\n    # do something\n
\n", "signature": "(\n self,\n count=30,\n offset=0,\n **kwargs\n) -> Iterator[TikTokApi.api.video.Video]", "funcdef": "def"}, {"fullname": "TikTokApi.api.trending", "modulename": "TikTokApi.api.trending", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.trending.Trending", "modulename": "TikTokApi.api.trending", "qualname": "Trending", "type": "class", "doc": "

Contains static methods related to trending.

\n"}, {"fullname": "TikTokApi.api.trending.Trending.__init__", "modulename": "TikTokApi.api.trending", "qualname": "Trending.__init__", "type": "function", "doc": "

\n", "signature": "()", "funcdef": "def"}, {"fullname": "TikTokApi.api.trending.Trending.videos", "modulename": "TikTokApi.api.trending", "qualname": "Trending.videos", "type": "function", "doc": "

Returns Videos that are trending on TikTok.

\n\n
    \n
  • Parameters:\n
      \n
    • count (int): The amount of videos you want returned.
    • \n
  • \n
\n", "signature": "(count=30, **kwargs) -> Iterator[TikTokApi.api.video.Video]", "funcdef": "def"}, {"fullname": "TikTokApi.api.user", "modulename": "TikTokApi.api.user", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.user.User", "modulename": "TikTokApi.api.user", "qualname": "User", "type": "class", "doc": "

A TikTok User.

\n\n

Example Usage

\n\n
user = api.user(username='therock')\n# or\nuser_id = '5831967'\nsec_uid = 'MS4wLjABAAAA-VASjiXTh7wDDyXvjk10VFhMWUAoxr8bgfO1kAL1-9s'\nuser = api.user(user_id=user_id, sec_uid=sec_uid)\n
\n"}, {"fullname": "TikTokApi.api.user.User.__init__", "modulename": "TikTokApi.api.user", "qualname": "User.__init__", "type": "function", "doc": "

You must provide the username or (user_id and sec_uid) otherwise this\nwill not function correctly.

\n", "signature": "(\n self,\n username: Optional[str] = None,\n user_id: Optional[str] = None,\n sec_uid: Optional[str] = None,\n data: Optional[dict] = None\n)", "funcdef": "def"}, {"fullname": "TikTokApi.api.user.User.user_id", "modulename": "TikTokApi.api.user", "qualname": "User.user_id", "type": "variable", "doc": "

The user ID of the user.

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.user.User.sec_uid", "modulename": "TikTokApi.api.user", "qualname": "User.sec_uid", "type": "variable", "doc": "

The sec UID of the user.

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.user.User.username", "modulename": "TikTokApi.api.user", "qualname": "User.username", "type": "variable", "doc": "

The username of the user.

\n", "annotation": ": str"}, {"fullname": "TikTokApi.api.user.User.as_dict", "modulename": "TikTokApi.api.user", "qualname": "User.as_dict", "type": "variable", "doc": "

The raw data associated with this user.

\n", "annotation": ": dict"}, {"fullname": "TikTokApi.api.user.User.info", "modulename": "TikTokApi.api.user", "qualname": "User.info", "type": "function", "doc": "

Returns a dictionary of TikTok's User object

\n\n

Example Usage

\n\n
user_data = api.user(username='therock').info()\n
\n", "signature": "(self, **kwargs)", "funcdef": "def"}, {"fullname": "TikTokApi.api.user.User.info_full", "modulename": "TikTokApi.api.user", "qualname": "User.info_full", "type": "function", "doc": "

Returns a dictionary of information associated with this User.\nIncludes statistics about this user.

\n\n

Example Usage

\n\n
user_data = api.user(username='therock').info_full()\n
\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.user.User.videos", "modulename": "TikTokApi.api.user", "qualname": "User.videos", "type": "function", "doc": "

Returns an iterator yielding Video objects.

\n\n
    \n
  • Parameters:\n
      \n
    • count (int): The amount of videos you want returned.
    • \n
    • cursor (int): The unix epoch to get videos since. TODO: Check this is right
    • \n
  • \n
\n\n

Example Usage

\n\n
user = api.user(username='therock')\nfor video in user.videos(count=100):\n    print(video.id)\n
\n", "signature": "(\n self,\n count=30,\n cursor=0,\n **kwargs\n) -> Iterator[TikTokApi.api.video.Video]", "funcdef": "def"}, {"fullname": "TikTokApi.api.user.User.liked", "modulename": "TikTokApi.api.user", "qualname": "User.liked", "type": "function", "doc": "

Returns a dictionary listing TikToks that a given a user has liked.

\n\n

Note: The user's likes must be public (which is not the default option)

\n\n
    \n
  • Parameters:\n
      \n
    • count (int): The amount of videos you want returned.
    • \n
    • cursor (int): The unix epoch to get videos since. TODO: Check this is right
    • \n
  • \n
\n\n

Example Usage

\n\n
for liked_video in api.user(username='public_likes'):\n    print(liked_video.id)\n
\n", "signature": "(\n self,\n count: int = 30,\n cursor: int = 0,\n **kwargs\n) -> Iterator[TikTokApi.api.video.Video]", "funcdef": "def"}, {"fullname": "TikTokApi.api.video", "modulename": "TikTokApi.api.video", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.api.video.Video", "modulename": "TikTokApi.api.video", "qualname": "Video", "type": "class", "doc": "

A TikTok Video class

\n\n

Example Usage

\n\n
video = api.video(id='7041997751718137094')\n
\n"}, {"fullname": "TikTokApi.api.video.Video.__init__", "modulename": "TikTokApi.api.video", "qualname": "Video.__init__", "type": "function", "doc": "

You must provide the id or a valid url, else this will fail.

\n", "signature": "(\n self,\n id: Optional[str] = None,\n url: Optional[str] = None,\n data: Optional[dict] = None\n)", "funcdef": "def"}, {"fullname": "TikTokApi.api.video.Video.id", "modulename": "TikTokApi.api.video", "qualname": "Video.id", "type": "variable", "doc": "

TikTok's ID of the Video

\n", "annotation": ": Optional[str]"}, {"fullname": "TikTokApi.api.video.Video.author", "modulename": "TikTokApi.api.video", "qualname": "Video.author", "type": "variable", "doc": "

The User who created the Video

\n", "annotation": ": Optional[TikTokApi.api.user.User]"}, {"fullname": "TikTokApi.api.video.Video.sound", "modulename": "TikTokApi.api.video", "qualname": "Video.sound", "type": "variable", "doc": "

The Sound that is associated with the Video

\n", "annotation": ": Optional[TikTokApi.api.sound.Sound]"}, {"fullname": "TikTokApi.api.video.Video.hashtags", "modulename": "TikTokApi.api.video", "qualname": "Video.hashtags", "type": "variable", "doc": "

A List of Hashtags on the Video

\n", "annotation": ": Optional[list[TikTokApi.api.hashtag.Hashtag]]"}, {"fullname": "TikTokApi.api.video.Video.as_dict", "modulename": "TikTokApi.api.video", "qualname": "Video.as_dict", "type": "variable", "doc": "

The raw data associated with this Video.

\n", "annotation": ": dict"}, {"fullname": "TikTokApi.api.video.Video.info", "modulename": "TikTokApi.api.video", "qualname": "Video.info", "type": "function", "doc": "

Returns a dictionary of TikTok's Video object.

\n\n

Example Usage

\n\n
video_data = api.video(id='7041997751718137094').info()\n
\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.video.Video.info_full", "modulename": "TikTokApi.api.video", "qualname": "Video.info_full", "type": "function", "doc": "

Returns a dictionary of all data associated with a TikTok Video.

\n\n

Example Usage

\n\n
video_data = api.video(id='7041997751718137094').info_full()\n
\n", "signature": "(self, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.api.video.Video.bytes", "modulename": "TikTokApi.api.video", "qualname": "Video.bytes", "type": "function", "doc": "

Returns the bytes of a TikTok Video.

\n\n

Example Usage

\n\n
video_bytes = api.video(id='7041997751718137094').bytes()\n\n# Saving The Video\nwith open('saved_video.mp4', 'wb') as output:\n    output.write(video_bytes)\n
\n", "signature": "(self, **kwargs) -> bytes", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities", "modulename": "TikTokApi.browser_utilities", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.browser_utilities.browser", "modulename": "TikTokApi.browser_utilities.browser", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.browser_utilities.browser.get_playwright", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "get_playwright", "type": "function", "doc": "

\n", "signature": "()", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser.browser", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser", "type": "class", "doc": "

Helper class that provides a standard way to create an ABC using\ninheritance.

\n", "bases": "TikTokApi.browser_utilities.browser_interface.BrowserInterface"}, {"fullname": "TikTokApi.browser_utilities.browser.browser.__init__", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.__init__", "type": "function", "doc": "

\n", "signature": "(self, **kwargs)", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser.browser.get_params", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.get_params", "type": "function", "doc": "

\n", "signature": "(self, page) -> None", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser.browser.gen_verifyFp", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.gen_verifyFp", "type": "function", "doc": "

\n", "signature": "(self)", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser.browser.sign_url", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.sign_url", "type": "function", "doc": "

\n", "signature": "(self, url, calc_tt_params=False, **kwargs)", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser.browser.find_redirect", "modulename": "TikTokApi.browser_utilities.browser", "qualname": "browser.find_redirect", "type": "function", "doc": "

\n", "signature": "(self, url)", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser_interface", "modulename": "TikTokApi.browser_utilities.browser_interface", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.browser_utilities.browser_interface.BrowserInterface", "modulename": "TikTokApi.browser_utilities.browser_interface", "qualname": "BrowserInterface", "type": "class", "doc": "

Helper class that provides a standard way to create an ABC using\ninheritance.

\n", "bases": "abc.ABC"}, {"fullname": "TikTokApi.browser_utilities.browser_interface.BrowserInterface.get_params", "modulename": "TikTokApi.browser_utilities.browser_interface", "qualname": "BrowserInterface.get_params", "type": "function", "doc": "

\n", "signature": "(self, page) -> None", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.browser_interface.BrowserInterface.sign_url", "modulename": "TikTokApi.browser_utilities.browser_interface", "qualname": "BrowserInterface.sign_url", "type": "function", "doc": "

\n", "signature": "(self, calc_tt_params=False, **kwargs)", "funcdef": "def"}, {"fullname": "TikTokApi.browser_utilities.get_acrawler", "modulename": "TikTokApi.browser_utilities.get_acrawler", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.exceptions", "modulename": "TikTokApi.exceptions", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.exceptions.TikTokCaptchaError", "modulename": "TikTokApi.exceptions", "qualname": "TikTokCaptchaError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.TikTokCaptchaError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "TikTokCaptchaError.__init__", "type": "function", "doc": "

\n", "signature": "(\n self,\n message='TikTok blocks this request displaying a Captcha \\nTip: Consider using a proxy or a custom_verify_fp as method parameters'\n)", "funcdef": "def"}, {"fullname": "TikTokApi.exceptions.GenericTikTokError", "modulename": "TikTokApi.exceptions", "qualname": "GenericTikTokError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.GenericTikTokError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "GenericTikTokError.__init__", "type": "function", "doc": "

\n", "signature": "(self, message)", "funcdef": "def"}, {"fullname": "TikTokApi.exceptions.TikTokNotFoundError", "modulename": "TikTokApi.exceptions", "qualname": "TikTokNotFoundError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.TikTokNotFoundError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "TikTokNotFoundError.__init__", "type": "function", "doc": "

\n", "signature": "(self, message='The requested object does not exists')", "funcdef": "def"}, {"fullname": "TikTokApi.exceptions.EmptyResponseError", "modulename": "TikTokApi.exceptions", "qualname": "EmptyResponseError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.EmptyResponseError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "EmptyResponseError.__init__", "type": "function", "doc": "

\n", "signature": "(self, message='TikTok sent no data back')", "funcdef": "def"}, {"fullname": "TikTokApi.exceptions.JSONDecodeFailure", "modulename": "TikTokApi.exceptions", "qualname": "JSONDecodeFailure", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.JSONDecodeFailure.__init__", "modulename": "TikTokApi.exceptions", "qualname": "JSONDecodeFailure.__init__", "type": "function", "doc": "

\n", "signature": "(self, message='TikTok sent invalid JSON back')", "funcdef": "def"}, {"fullname": "TikTokApi.exceptions.TikTokNotAvailableError", "modulename": "TikTokApi.exceptions", "qualname": "TikTokNotAvailableError", "type": "class", "doc": "

Common base class for all non-exit exceptions.

\n", "bases": "builtins.Exception"}, {"fullname": "TikTokApi.exceptions.TikTokNotAvailableError.__init__", "modulename": "TikTokApi.exceptions", "qualname": "TikTokNotAvailableError.__init__", "type": "function", "doc": "

\n", "signature": "(self, message='The requested object is not available in this region')", "funcdef": "def"}, {"fullname": "TikTokApi.helpers", "modulename": "TikTokApi.helpers", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.helpers.extract_tag_contents", "modulename": "TikTokApi.helpers", "qualname": "extract_tag_contents", "type": "function", "doc": "

\n", "signature": "(html)", "funcdef": "def"}, {"fullname": "TikTokApi.helpers.extract_video_id_from_url", "modulename": "TikTokApi.helpers", "qualname": "extract_video_id_from_url", "type": "function", "doc": "

\n", "signature": "(url)", "funcdef": "def"}, {"fullname": "TikTokApi.tiktok", "modulename": "TikTokApi.tiktok", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi", "type": "class", "doc": "

\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.__init__", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.__init__", "type": "function", "doc": "

The TikTokApi class. Used to interact with TikTok. This is a singleton\n class to prevent issues from arising with playwright

\n\n
Parameters
\n\n
    \n
  • logging_level: The logging level you want the program to run at, optional\nThese are the standard python logging module's levels.

  • \n
  • request_delay: The amount of time in seconds to wait before making a request, optional\nThis is used to throttle your own requests as you may end up making too\nmany requests to TikTok for your IP.

  • \n
  • custom_device_id: A TikTok parameter needed to download videos, optional\nThe code generates these and handles these pretty well itself, however\nfor some things such as video download you will need to set a consistent\none of these. All the methods take this as a optional parameter, however\nit's cleaner code to store this at the instance level. You can override\nthis at the specific methods.

  • \n
  • generate_static_device_id: A parameter that generates a custom_device_id at the instance level\nUse this if you want to download videos from a script but don't want to generate\nyour own custom_device_id parameter.

  • \n
  • custom_verify_fp: A TikTok parameter needed to work most of the time, optional\nTo get this parameter look at this video\nI recommend watching the entire thing, as it will help setup this package. All\nthe methods take this as a optional parameter, however it's cleaner code\nto store this at the instance level. You can override this at the specific\nmethods.

    \n\n

    You can use the following to generate \"\".join(random.choice(string.digits)\nfor num in range(19))

  • \n
  • use_test_endpoints: Send requests to TikTok's test endpoints, optional\nThis parameter when set to true will make requests to TikTok's testing\nendpoints instead of the live site. I can't guarantee this will work\nin the future, however currently basically any custom_verify_fp will\nwork here which is helpful.

  • \n
  • proxy: A string containing your proxy address, optional\nIf you want to do a lot of scraping of TikTok endpoints you'll likely\nneed a proxy.

    \n\n

    Ex: \"https://0.0.0.0:8080\"

    \n\n

    All the methods take this as a optional parameter, however it's cleaner code\nto store this at the instance level. You can override this at the specific\nmethods.

  • \n
  • use_selenium: Option to use selenium over playwright, optional\nPlaywright is selected by default and is the one that I'm designing the\npackage to be compatable for, however if playwright doesn't work on\nyour machine feel free to set this to True.

  • \n
  • executablePath: The location of the driver, optional\nThis shouldn't be needed if you're using playwright

  • \n
  • **kwargs\nParameters that are passed on to basically every module and methods\nthat interact with this main class. These may or may not be documented\nin other places.

  • \n
\n", "signature": "(cls, logging_level=30, *args, **kwargs)", "funcdef": "def"}, {"fullname": "TikTokApi.tiktok.TikTokApi.logger", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.logger", "type": "variable", "doc": "

\n", "annotation": ": ClassVar[logging.Logger]", "default_value": " = "}, {"fullname": "TikTokApi.tiktok.TikTokApi.user", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.user", "type": "class", "doc": "

A TikTok User.

\n\n

Example Usage

\n\n
user = api.user(username='therock')\n# or\nuser_id = '5831967'\nsec_uid = 'MS4wLjABAAAA-VASjiXTh7wDDyXvjk10VFhMWUAoxr8bgfO1kAL1-9s'\nuser = api.user(user_id=user_id, sec_uid=sec_uid)\n
\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.search", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.search", "type": "class", "doc": "

Contains static methods about searching.

\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.sound", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.sound", "type": "class", "doc": "

A TikTok Sound/Music/Song.

\n\n

Example Usage

\n\n
song = api.song(id='7016547803243022337')\n
\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.hashtag", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.hashtag", "type": "class", "doc": "

A TikTok Hashtag/Challenge.

\n\n

Example Usage

\n\n
hashtag = api.hashtag(name='funny')\n
\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.video", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.video", "type": "class", "doc": "

A TikTok Video class

\n\n

Example Usage

\n\n
video = api.video(id='7041997751718137094')\n
\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.trending", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.trending", "type": "class", "doc": "

Contains static methods related to trending.

\n"}, {"fullname": "TikTokApi.tiktok.TikTokApi.get_data", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_data", "type": "function", "doc": "

Makes requests to TikTok and returns their JSON.

\n\n

This is all handled by the package so it's unlikely\nyou will need to use this.

\n", "signature": "(self, path, use_desktop_base_url=False, **kwargs) -> dict", "funcdef": "def"}, {"fullname": "TikTokApi.tiktok.TikTokApi.external_signer", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.external_signer", "type": "function", "doc": "

Makes requests to an external signer instead of using a browser.

\n\n
Parameters
\n\n
    \n
  • url: The server to make requests to\nThis server is designed to sign requests. You can find an example\nof this signature server in the examples folder.

  • \n
  • custom_device_id: A TikTok parameter needed to download videos\nThe code generates these and handles these pretty well itself, however\nfor some things such as video download you will need to set a consistent\none of these.

  • \n
  • custom_verify_fp: A TikTok parameter needed to work most of the time,\nTo get this parameter look at this video\nI recommend watching the entire thing, as it will help setup this package.

  • \n
\n", "signature": "(self, url, custom_device_id=None, verifyFp=None)", "funcdef": "def"}, {"fullname": "TikTokApi.tiktok.TikTokApi.get_bytes", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.get_bytes", "type": "function", "doc": "

Returns TikTok's response as bytes, similar to get_data

\n", "signature": "(self, **kwargs) -> bytes", "funcdef": "def"}, {"fullname": "TikTokApi.tiktok.TikTokApi.generate_device_id", "modulename": "TikTokApi.tiktok", "qualname": "TikTokApi.generate_device_id", "type": "function", "doc": "

Generates a valid device_id for other methods. Pass this as the custom_device_id field to download videos

\n", "signature": "()", "funcdef": "def"}, {"fullname": "TikTokApi.utilities", "modulename": "TikTokApi.utilities", "type": "module", "doc": "

\n"}, {"fullname": "TikTokApi.utilities.LOGGER_NAME", "modulename": "TikTokApi.utilities", "qualname": "LOGGER_NAME", "type": "variable", "doc": "

\n", "annotation": ": str", "default_value": " = 'TikTokApi'"}, {"fullname": "TikTokApi.utilities.update_messager", "modulename": "TikTokApi.utilities", "qualname": "update_messager", "type": "function", "doc": "

\n", "signature": "()", "funcdef": "def"}, {"fullname": "TikTokApi.utilities.check", "modulename": "TikTokApi.utilities", "qualname": "check", "type": "function", "doc": "

\n", "signature": "(name)", "funcdef": "def"}, {"fullname": "TikTokApi.utilities.check_future_deprecation", "modulename": "TikTokApi.utilities", "qualname": "check_future_deprecation", "type": "function", "doc": "

\n", "signature": "()", "funcdef": "def"}]; // mirrored in build-search-index.js (part 1) // Also split on html tags. this is a cheap heuristic, but good enough. From 85783ac141850965b8c2a9390bd67a22d90299ef Mon Sep 17 00:00:00 2001 From: David Teather <34144122+davidteather@users.noreply.github.com> Date: Wed, 26 Jan 2022 15:26:52 -0600 Subject: [PATCH 12/17] Fix typing --- TikTokApi/tiktok.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/TikTokApi/tiktok.py b/TikTokApi/tiktok.py index 637c5fbf..3d80f706 100644 --- a/TikTokApi/tiktok.py +++ b/TikTokApi/tiktok.py @@ -4,7 +4,7 @@ import random import string import time -from typing import ClassVar +from typing import ClassVar, Optional from urllib.parse import quote, urlencode import requests @@ -497,7 +497,7 @@ def generate_device_id(): # PRIVATE METHODS # - def _format_proxy(self, proxy) -> dict | None: + def _format_proxy(self, proxy) -> Optional[dict]: """ Formats the proxy object """ From df4eb23455aa607f489d6f97cf13a2aa023515d1 Mon Sep 17 00:00:00 2001 From: David Teather <34144122+davidteather@users.noreply.github.com> Date: Thu, 27 Jan 2022 16:50:51 -0600 Subject: [PATCH 13/17] Investigated discover_type, still doing work on it though --- TikTokApi/api/search.py | 64 +++++++++++++++++++++++++++++++++++------ TikTokApi/tiktok.py | 9 ++---- tests/test_search.py | 28 ++++++++++++++++++ 3 files changed, 87 insertions(+), 14 deletions(-) diff --git a/TikTokApi/api/search.py b/TikTokApi/api/search.py index 6dd3a2d1..4764cb27 100644 --- a/TikTokApi/api/search.py +++ b/TikTokApi/api/search.py @@ -2,11 +2,12 @@ from urllib.parse import urlencode -from typing import TYPE_CHECKING, Iterator +from typing import TYPE_CHECKING, Iterator, Type from .user import User from .sound import Sound from .hashtag import Hashtag +from .video import Video if TYPE_CHECKING: from ..tiktok import TikTokApi @@ -20,7 +21,7 @@ class Search: parent: TikTokApi @staticmethod - def users(search_term, count=28, **kwargs) -> Iterator[User]: + def users_old(search_term, count=28, **kwargs) -> Iterator[User]: """ Searches for users. @@ -37,7 +38,7 @@ def users(search_term, count=28, **kwargs) -> Iterator[User]: return Search.discover_type(search_term, prefix="user", count=count, **kwargs) @staticmethod - def sounds(search_term, count=28, **kwargs) -> Iterator[Sound]: + def sounds_old(search_term, count=28, **kwargs) -> Iterator[Sound]: """ Searches for sounds. @@ -54,7 +55,7 @@ def sounds(search_term, count=28, **kwargs) -> Iterator[Sound]: return Search.discover_type(search_term, prefix="music", count=count, **kwargs) @staticmethod - def hashtags(search_term, count=28, **kwargs) -> Iterator[Hashtag]: + def hashtags_old(search_term, count=28, **kwargs) -> Iterator[Hashtag]: """ Searches for hashtags/challenges. @@ -137,7 +138,27 @@ def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> Iterator offset = int(data["offset"]) @staticmethod - def users_alternate(search_term, count=28, offset=0, **kwargs) -> Iterator[User]: + def videos(search_term, count=28, offset=0, **kwargs) -> Iterator[Video]: + """ + Searches for Videos + + - Parameters: + - search_term (str): The phrase you want to search for. + - count (int): The amount of videos you want returned. + TODO: Figure out offset + + Example Usage + ```py + for video in api.search.videos('therock'): + # do something + ``` + """ + return Search.search_type( + search_term, "item", count=count, offset=offset, **kwargs + ) + + @staticmethod + def users(search_term, count=28, offset=0, **kwargs) -> Iterator[User]: """ Searches for users using an alternate endpoint than Search.users @@ -151,6 +172,23 @@ def users_alternate(search_term, count=28, offset=0, **kwargs) -> Iterator[User] # do something ``` """ + return Search.search_type( + search_term, "user", count=count, offset=offset, **kwargs + ) + + @staticmethod + def search_type(search_term, obj_type, count=28, offset=0, **kwargs) -> Iterator: + """ + Searches for users using an alternate endpoint than Search.users + + - Parameters: + - search_term (str): The phrase you want to search for. + - count (int): The amount of videos you want returned. + - obj_type (str): user | item + + Just use .video & .users + ``` + """ ( region, language, @@ -174,20 +212,30 @@ def users_alternate(search_term, count=28, offset=0, **kwargs) -> Iterator[User] query = { "keyword": search_term, "cursor": cursor, - "app_language": "en", + "app_language": Search.parent.language, } path = "api/search/{}/full/?{}&{}".format( - "user", Search.parent._add_url_params(), urlencode(query) + obj_type, Search.parent._add_url_params(), urlencode(query) ) + if obj_type == "user": + subdomain = "www" + elif obj_type == "item": + subdomain = "us" + else: + raise TypeError("invalid obj_type") + api_response = Search.parent.get_data( - path, use_desktop_base_url=True, ttwid=ttwid, **kwargs + path, subdomain=subdomain, ttwid=ttwid, **kwargs ) # When I move to 3.10+ support make this a match switch. for result in api_response.get("user_list", []): yield User(data=result) + for result in api_response.get("item_list", []): + yield Video(data=result) + if api_response.get("has_more", 0) == 0: Search.parent.logger.info( "TikTok is not sending videos beyond this point." diff --git a/TikTokApi/tiktok.py b/TikTokApi/tiktok.py index 3d80f706..449db5a8 100644 --- a/TikTokApi/tiktok.py +++ b/TikTokApi/tiktok.py @@ -166,7 +166,7 @@ def _initialize(self, logging_level=logging.WARNING, **kwargs): self.region = "US" self.language = "en" - def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict: + def get_data(self, path, subdomain="m", **kwargs) -> dict: """Makes requests to TikTok and returns their JSON. This is all handled by the package so it's unlikely @@ -197,10 +197,7 @@ def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict: tt_params = None send_tt_params = kwargs.get("send_tt_params", False) - if use_desktop_base_url: - full_url = DESKTOP_BASE_URL + path - else: - full_url = BASE_URL + path + full_url = f"https://{subdomain}.tiktok.com/" + path if self.signer_url is None: kwargs["custom_verify_fp"] = verifyFp @@ -236,7 +233,7 @@ def get_data(self, path, use_desktop_base_url=False, **kwargs) -> dict: ) csrf_token = None - if not use_desktop_base_url: + if subdomain == "m": csrf_session_id = h.cookies["csrf_session_id"] csrf_token = h.headers["X-Ware-Csrf-Token"].split(",")[1] kwargs["csrf_session_id"] = csrf_session_id diff --git a/tests/test_search.py b/tests/test_search.py index 91972906..ff39e5eb 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -6,19 +6,43 @@ def test_discover_type(): count = 0 + found_ids = [] for result in api.search.users("therock", count=50): + if result.user_id in found_ids: + raise Exception("Multiple Users on search.user") + found_ids.append(result.user_id) count += 1 assert count >= 50 count = 0 + found_ids for result in api.search.sounds("funny", count=50): + if result.id in found_ids: + raise Exception("Multiple Sounds on search.sound") + found_ids.append(result.id) count += 1 assert count >= 50 count = 0 + found_ids = [] for result in api.search.hashtags("funny", count=50): + if result.id in found_ids: + raise Exception("Multiple Users on search.user") + found_ids.append(result.id) + count += 1 + + assert count >= 50 + + +def test_video(): + count = 0 + found_ids = [] + for video in api.search.videos("therock", count=50): + if video.id in found_ids: + raise Exception("Duplicate Video on search.videos") + found_ids.append(video.id) count += 1 assert count >= 50 @@ -26,7 +50,11 @@ def test_discover_type(): def test_users_alternate(): count = 0 + found_ids = [] for user in api.search.users_alternate("therock", count=50): + if user.user_id in found_ids: + raise Exception("Multiple Users on search.user") + found_ids.append(user.user_id) count += 1 assert count >= 50 From 16b870c6580bdabab63e0cbbc9aa4e894ec1d31e Mon Sep 17 00:00:00 2001 From: davidteather Date: Thu, 27 Jan 2022 17:44:54 -0600 Subject: [PATCH 14/17] Some TODOS --- TikTokApi/api/hashtag.py | 1 + TikTokApi/tiktok.py | 1 + 2 files changed, 2 insertions(+) diff --git a/TikTokApi/api/hashtag.py b/TikTokApi/api/hashtag.py index 3958d320..a56b8a03 100644 --- a/TikTokApi/api/hashtag.py +++ b/TikTokApi/api/hashtag.py @@ -159,6 +159,7 @@ def __str__(self): return f"TikTokApi.hashtag(id='{self.id}', name='{self.name}')" def __getattr__(self, name): + # TODO: Maybe switch to using @property instead if name in ["id", "name", "as_dict"]: self.as_dict = self.info() self.__extract_from_data() diff --git a/TikTokApi/tiktok.py b/TikTokApi/tiktok.py index 637c5fbf..f69fdebe 100644 --- a/TikTokApi/tiktok.py +++ b/TikTokApi/tiktok.py @@ -510,6 +510,7 @@ def _format_proxy(self, proxy) -> dict | None: # Process the kwargs def _process_kwargs(self, kwargs): + # TODO: Just return this as a dict, then only use the keys you need region = kwargs.get("region", "US") language = kwargs.get("language", "en") proxy = kwargs.get("proxy", None) From a6c1dd99dd92239c61ad64ba8f78885512dbf66a Mon Sep 17 00:00:00 2001 From: davidteather Date: Thu, 27 Jan 2022 20:47:52 -0600 Subject: [PATCH 15/17] Move old search functions to trending & fix process_kwargs --- TikTokApi/api/hashtag.py | 20 ++---- TikTokApi/api/search.py | 129 +------------------------------------- TikTokApi/api/sound.py | 20 ++---- TikTokApi/api/trending.py | 118 ++++++++++++++++++++++++++++++---- TikTokApi/api/user.py | 32 +++------- TikTokApi/api/video.py | 20 ++---- TikTokApi/tiktok.py | 40 ++++++------ tests/test_search.py | 36 +---------- tests/test_trending.py | 32 ++++++++++ 9 files changed, 184 insertions(+), 263 deletions(-) diff --git a/TikTokApi/api/hashtag.py b/TikTokApi/api/hashtag.py index a56b8a03..054391b6 100644 --- a/TikTokApi/api/hashtag.py +++ b/TikTokApi/api/hashtag.py @@ -61,14 +61,8 @@ def info_full(self, **kwargs) -> dict: hashtag_data = api.hashtag(name='funny').info_full() ``` """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self.parent._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id + processed = self.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = processed.device_id if self.name is not None: query = {"challengeName": self.name} @@ -102,14 +96,8 @@ def videos(self, count=30, offset=0, **kwargs) -> Iterator[Video]: # do something ``` """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self.parent._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id + processed = self.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = processed.device_id if self.id is None: self.id = self.info()["id"] diff --git a/TikTokApi/api/search.py b/TikTokApi/api/search.py index 4764cb27..d64043ce 100644 --- a/TikTokApi/api/search.py +++ b/TikTokApi/api/search.py @@ -20,123 +20,6 @@ class Search: parent: TikTokApi - @staticmethod - def users_old(search_term, count=28, **kwargs) -> Iterator[User]: - """ - Searches for users. - - - Parameters: - - search_term (str): The phrase you want to search for. - - count (int): The amount of videos you want returned. - - Example Usage - ```py - for user in api.search.users('therock'): - # do something - ``` - """ - return Search.discover_type(search_term, prefix="user", count=count, **kwargs) - - @staticmethod - def sounds_old(search_term, count=28, **kwargs) -> Iterator[Sound]: - """ - Searches for sounds. - - - Parameters: - - search_term (str): The phrase you want to search for. - - count (int): The amount of videos you want returned. - - Example Usage - ```py - for user in api.search.sounds('funny'): - # do something - ``` - """ - return Search.discover_type(search_term, prefix="music", count=count, **kwargs) - - @staticmethod - def hashtags_old(search_term, count=28, **kwargs) -> Iterator[Hashtag]: - """ - Searches for hashtags/challenges. - - - Parameters: - - search_term (str): The phrase you want to search for. - - count (int): The amount of videos you want returned. - - Example Usage - ```py - for user in api.search.hashtags('funny'): - # do something - ``` - """ - return Search.discover_type( - search_term, prefix="challenge", count=count, **kwargs - ) - - @staticmethod - def discover_type(search_term, prefix, count=28, offset=0, **kwargs) -> Iterator: - """ - Searches for a specific type of object. - You should instead use the users/sounds/hashtags as they all use data - from this function. - - - Parameters: - - search_term (str): The phrase you want to search for. - - prefix (str): either user|music|challenge - - count (int): The amount of videos you want returned. - - Example Usage - ```py - for user in api.search.discover_type('therock', 'user'): - # do something - ``` - - """ - # TODO: Investigate if this is actually working as expected. Doesn't seem to be - ( - region, - language, - proxy, - maxCount, - device_id, - ) = Search.parent._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id - - cursor = offset - page_size = 28 - - while cursor - offset < count: - query = { - "discoverType": 0, - "needItemList": False, - "keyWord": search_term, - "offset": cursor, - "count": page_size, - "useRecommend": False, - "language": "en", - } - path = "api/discover/{}/?{}&{}".format( - prefix, Search.parent._add_url_params(), urlencode(query) - ) - data = Search.parent.get_data(path, **kwargs) - - for x in data.get("userInfoList", []): - yield User(data=x["user"]) - - for x in data.get("musicInfoList", []): - yield Sound(data=x["music"]) - - for x in data.get("challengeInfoList", []): - yield Hashtag(data=x["challenge"]) - - if int(data["offset"]) <= offset: - Search.parent.logger.info( - "TikTok is not sending videos beyond this point." - ) - return - - offset = int(data["offset"]) - @staticmethod def videos(search_term, count=28, offset=0, **kwargs) -> Iterator[Video]: """ @@ -189,20 +72,14 @@ def search_type(search_term, obj_type, count=28, offset=0, **kwargs) -> Iterator Just use .video & .users ``` """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = Search.parent._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id + processed = Search.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = processed.device_id cursor = offset spawn = requests.head( "https://www.tiktok.com", - proxies=Search.parent._format_proxy(proxy), + proxies=Search.parent._format_proxy(processed.proxy), **Search.parent.requests_extra_kwargs ) ttwid = spawn.cookies["ttwid"] diff --git a/TikTokApi/api/sound.py b/TikTokApi/api/sound.py index e016fac5..5ed7c080 100644 --- a/TikTokApi/api/sound.py +++ b/TikTokApi/api/sound.py @@ -66,14 +66,8 @@ def info(self, use_html=False, **kwargs) -> dict: if use_html: return self.info_full(**kwargs)["musicInfo"] - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self.parent._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id + processed = self.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = processed.device_id path = "node/share/music/-{}?{}".format(self.id, self.parent._add_url_params()) res = self.parent.get_data(path, **kwargs) @@ -125,14 +119,8 @@ def videos(self, count=30, offset=0, **kwargs) -> Iterator[Video]: # do something ``` """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self.parent._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id + processed = self.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = processed.device_id cursor = offset page_size = 30 diff --git a/TikTokApi/api/trending.py b/TikTokApi/api/trending.py index 46431d94..619cd00e 100644 --- a/TikTokApi/api/trending.py +++ b/TikTokApi/api/trending.py @@ -5,6 +5,9 @@ from urllib.parse import urlencode from .video import Video +from .sound import Sound +from .user import User +from .hashtag import Hashtag from typing import TYPE_CHECKING, Iterator @@ -17,6 +20,103 @@ class Trending: parent: TikTokApi + @staticmethod + def users(**kwargs) -> Iterator[User]: + """ + Trending users + + Example Usage + ```py + for user in api.trending.users('therock'): + # do something + ``` + """ + return Trending.discover_type(prefix="user", **kwargs) + + @staticmethod + def sounds(**kwargs) -> Iterator[Sound]: + """ + Trending sounds + + Example Usage + ```py + for user in api.trending.sounds('funny'): + # do something + ``` + """ + return Trending.discover_type(prefix="music", **kwargs) + + @staticmethod + def hashtags(**kwargs) -> Iterator[Hashtag]: + """ + Trending hashtags + + Example Usage + ```py + for user in api.trending.hashtags('funny'): + # do something + ``` + """ + return Trending.discover_type(prefix="challenge", **kwargs) + + @staticmethod + def discover_type(prefix, count=28, offset=0, **kwargs) -> Iterator: + """ + Returns trending objects. + + You should instead use the users/sounds/hashtags as they all use data + from this function. + + - Parameters: + - search_term (str): The phrase you want to search for. + - prefix (str): either user|music|challenge + + Example Usage + ```py + for user in api.search.discover_type('therock', 'user'): + # do something + ``` + + """ + # TODO: Investigate if this is actually working as expected. Doesn't seem to be, check offset + processed = Trending.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = processed.device_id + + cursor = offset + page_size = 28 + + while cursor - offset < count: + query = { + "discoverType": 0, + "needItemList": False, + "keyWord": "f", # Keyword is a required param, but doesn't do anything anymore. + "offset": cursor, + "count": page_size, + "useRecommend": False, + "language": "en", + } + path = "node/share/discover/{}/?{}&{}".format( + prefix, Trending.parent._add_url_params(), urlencode(query) + ) + data = Trending.parent.get_data(path, **kwargs) + + for x in data.get("userInfoList", []): + yield User(data=x["user"]) + + for x in data.get("musicInfoList", []): + yield Sound(data=x["music"]) + + for x in data.get("challengeInfoList", []): + yield Hashtag(data=x["challenge"]) + + if int(data["offset"]) <= offset: + Trending.parent.logger.info( + "TikTok is not sending videos beyond this point." + ) + return + + offset = int(data["offset"]) + @staticmethod def videos(count=30, **kwargs) -> Iterator[Video]: """ @@ -26,18 +126,12 @@ def videos(count=30, **kwargs) -> Iterator[Video]: - count (int): The amount of videos you want returned. """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = Trending.parent._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id + processed = Trending.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = processed.device_id spawn = requests.head( "https://www.tiktok.com", - proxies=Trending.parent._format_proxy(proxy), + proxies=Trending.parent._format_proxy(processed.proxy), **Trending.parent.requests_extra_kwargs, ) ttwid = spawn.cookies["ttwid"] @@ -52,9 +146,9 @@ def videos(count=30, **kwargs) -> Iterator[Video]: "sourceType": 12, "itemID": 1, "insertedItemID": "", - "region": region, - "priority_region": region, - "language": language, + "region": processed.region, + "priority_region": processed.region, + "language": processed.language, } path = "api/recommend/item_list/?{}&{}".format( Trending.parent._add_url_params(), urlencode(query) diff --git a/TikTokApi/api/user.py b/TikTokApi/api/user.py index 7000441f..2a1b1cba 100644 --- a/TikTokApi/api/user.py +++ b/TikTokApi/api/user.py @@ -126,14 +126,8 @@ def videos(self, count=30, cursor=0, **kwargs) -> Iterator[Video]: print(video.id) ``` """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = User.parent._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id + processed = User.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = processed.device_id if not self.user_id and not self.sec_uid: self.__find_attributes() @@ -150,9 +144,9 @@ def videos(self, count=30, cursor=0, **kwargs) -> Iterator[Video]: "secUid": self.sec_uid, "sourceType": 8, "appId": 1233, - "region": region, - "priority_region": region, - "language": language, + "region": processed.region, + "priority_region": processed.region, + "language": processed.language, } path = "api/post/item_list/?{}&{}".format( User.parent._add_url_params(), urlencode(query) @@ -190,14 +184,8 @@ def liked(self, count: int = 30, cursor: int = 0, **kwargs) -> Iterator[Video]: print(liked_video.id) ``` """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self.parent._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id + processed = User.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = processed.device_id amount_yielded = 0 first = True @@ -214,9 +202,9 @@ def liked(self, count: int = 30, cursor: int = 0, **kwargs) -> Iterator[Video]: "cursor": cursor, "sourceType": 9, "appId": 1233, - "region": region, - "priority_region": region, - "language": language, + "region": processed.region, + "priority_region": processed.region, + "language": processed.language, } path = "api/favorite/item_list/?{}&{}".format( User.parent._add_url_params(), urlencode(query) diff --git a/TikTokApi/api/video.py b/TikTokApi/api/video.py index c0b9218e..7285e79c 100644 --- a/TikTokApi/api/video.py +++ b/TikTokApi/api/video.py @@ -76,14 +76,8 @@ def info_full(self, **kwargs) -> dict: video_data = api.video(id='7041997751718137094').info_full() ``` """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self.parent._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id + processed = self.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = processed.device_id device_id = kwargs.get("custom_device_id", None) query = { @@ -108,14 +102,8 @@ def bytes(self, **kwargs) -> bytes: output.write(video_bytes) ``` """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self.parent._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id + processed = self.parent._process_kwargs(kwargs) + kwargs["custom_device_id"] = processed.device_id video_data = self.info(**kwargs) download_url = video_data["video"]["playAddr"] diff --git a/TikTokApi/tiktok.py b/TikTokApi/tiktok.py index 4e1367e5..4c830955 100644 --- a/TikTokApi/tiktok.py +++ b/TikTokApi/tiktok.py @@ -20,6 +20,7 @@ from .exceptions import * from .utilities import LOGGER_NAME, update_messager from .browser_utilities.browser import browser +from dataclasses import dataclass os.environ["no_proxy"] = "127.0.0.1,localhost" @@ -172,14 +173,8 @@ def get_data(self, path, subdomain="m", **kwargs) -> dict: This is all handled by the package so it's unlikely you will need to use this. """ - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id + processed = self._process_kwargs(kwargs) + kwargs["custom_device_id"] = processed.device_id if self.request_delay is not None: time.sleep(self.request_delay) @@ -228,7 +223,7 @@ def get_data(self, path, subdomain="m", **kwargs) -> dict: h = requests.head( url, headers={"x-secsdk-csrf-version": "1.2.5", "x-secsdk-csrf-request": "1"}, - proxies=self._format_proxy(proxy), + proxies=self._format_proxy(processed.proxy), **self.requests_extra_kwargs, ) @@ -262,7 +257,7 @@ def get_data(self, path, subdomain="m", **kwargs) -> dict: url, headers=headers, cookies=self._get_cookies(**kwargs), - proxies=self._format_proxy(proxy), + proxies=self._format_proxy(processed.proxy), **self.requests_extra_kwargs, ) @@ -440,14 +435,8 @@ def _get_cookies(self, **kwargs): def get_bytes(self, **kwargs) -> bytes: """Returns TikTok's response as bytes, similar to get_data""" - ( - region, - language, - proxy, - maxCount, - device_id, - ) = self._process_kwargs(kwargs) - kwargs["custom_device_id"] = device_id + processed = self._process_kwargs(kwargs) + kwargs["custom_device_id"] = processed.device_id if self.signer_url is None: verify_fp, device_id, signature, _ = self.browser.sign_url( calc_tt_params=False, **kwargs @@ -480,7 +469,7 @@ def get_bytes(self, **kwargs) -> bytes: "Referer": "https://www.tiktok.com/", "User-Agent": user_agent, }, - proxies=self._format_proxy(proxy), + proxies=self._format_proxy(processed.proxy), cookies=self._get_cookies(**kwargs), ) return r.content @@ -511,7 +500,6 @@ def _process_kwargs(self, kwargs): region = kwargs.get("region", "US") language = kwargs.get("language", "en") proxy = kwargs.get("proxy", None) - maxCount = kwargs.get("maxCount", 35) if kwargs.get("custom_device_id", None) != None: device_id = kwargs.get("custom_device_id") @@ -520,7 +508,17 @@ def _process_kwargs(self, kwargs): device_id = self.custom_device_id else: device_id = "".join(random.choice(string.digits) for num in range(19)) - return region, language, proxy, maxCount, device_id + + @dataclass + class ProcessedKwargs: + region: str + language: str + proxy: str + device_id: str + + return ProcessedKwargs( + region=region, language=language, proxy=proxy, device_id=device_id + ) def __get_js(self, proxy=None) -> str: return requests.get( diff --git a/tests/test_search.py b/tests/test_search.py index ff39e5eb..1174e165 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -4,38 +4,6 @@ api = TikTokApi(custom_verify_fp=os.environ.get("verifyFp", None)) -def test_discover_type(): - count = 0 - found_ids = [] - for result in api.search.users("therock", count=50): - if result.user_id in found_ids: - raise Exception("Multiple Users on search.user") - found_ids.append(result.user_id) - count += 1 - - assert count >= 50 - - count = 0 - found_ids - for result in api.search.sounds("funny", count=50): - if result.id in found_ids: - raise Exception("Multiple Sounds on search.sound") - found_ids.append(result.id) - count += 1 - - assert count >= 50 - - count = 0 - found_ids = [] - for result in api.search.hashtags("funny", count=50): - if result.id in found_ids: - raise Exception("Multiple Users on search.user") - found_ids.append(result.id) - count += 1 - - assert count >= 50 - - def test_video(): count = 0 found_ids = [] @@ -48,10 +16,10 @@ def test_video(): assert count >= 50 -def test_users_alternate(): +def test_users(): count = 0 found_ids = [] - for user in api.search.users_alternate("therock", count=50): + for user in api.search.users("therock", count=50): if user.user_id in found_ids: raise Exception("Multiple Users on search.user") found_ids.append(user.user_id) diff --git a/tests/test_trending.py b/tests/test_trending.py index d134c2d4..ea26e13b 100644 --- a/tests/test_trending.py +++ b/tests/test_trending.py @@ -10,3 +10,35 @@ def test_trending_videos(): count += 1 assert count >= 100 + + +def test_discover_type(): + count = 0 + found_ids = [] + for result in api.trending.users(): + if result.user_id in found_ids: + raise Exception("Multiple Users on trending.user") + found_ids.append(result.user_id) + count += 1 + + assert count >= 50 + + count = 0 + found_ids + for result in api.trending.sounds(): + if result.id in found_ids: + raise Exception("Multiple Sounds on trending.sound") + found_ids.append(result.id) + count += 1 + + assert count >= 50 + + count = 0 + found_ids = [] + for result in api.trending.hashtags(): + if result.id in found_ids: + raise Exception("Multiple Users on trending.user") + found_ids.append(result.id) + count += 1 + + assert count >= 50 From b2ce6d279660c13fbd152437088c0652ecedba6b Mon Sep 17 00:00:00 2001 From: davidteather Date: Thu, 27 Jan 2022 21:20:53 -0600 Subject: [PATCH 16/17] Private self variables --- TikTokApi/api/search.py | 4 +- TikTokApi/api/sound.py | 2 +- TikTokApi/api/trending.py | 99 +---------------------------- TikTokApi/api/user.py | 4 +- TikTokApi/tiktok.py | 129 ++++++++++++++++++-------------------- tests/test_search.py | 4 -- tests/test_trending.py | 34 +--------- 7 files changed, 68 insertions(+), 208 deletions(-) diff --git a/TikTokApi/api/search.py b/TikTokApi/api/search.py index d64043ce..492ddbe0 100644 --- a/TikTokApi/api/search.py +++ b/TikTokApi/api/search.py @@ -80,7 +80,7 @@ def search_type(search_term, obj_type, count=28, offset=0, **kwargs) -> Iterator spawn = requests.head( "https://www.tiktok.com", proxies=Search.parent._format_proxy(processed.proxy), - **Search.parent.requests_extra_kwargs + **Search.parent._requests_extra_kwargs ) ttwid = spawn.cookies["ttwid"] @@ -89,7 +89,7 @@ def search_type(search_term, obj_type, count=28, offset=0, **kwargs) -> Iterator query = { "keyword": search_term, "cursor": cursor, - "app_language": Search.parent.language, + "app_language": Search.parent._language, } path = "api/search/{}/full/?{}&{}".format( obj_type, Search.parent._add_url_params(), urlencode(query) diff --git a/TikTokApi/api/sound.py b/TikTokApi/api/sound.py index 5ed7c080..66e2dc3a 100644 --- a/TikTokApi/api/sound.py +++ b/TikTokApi/api/sound.py @@ -99,7 +99,7 @@ def info_full(self, **kwargs) -> dict: }, proxies=self.parent._format_proxy(kwargs.get("proxy", None)), cookies=self.parent._get_cookies(**kwargs), - **self.parent.requests_extra_kwargs, + **self.parent._requests_extra_kwargs, ) data = extract_tag_contents(r.text) diff --git a/TikTokApi/api/trending.py b/TikTokApi/api/trending.py index 619cd00e..7841a5b5 100644 --- a/TikTokApi/api/trending.py +++ b/TikTokApi/api/trending.py @@ -20,103 +20,6 @@ class Trending: parent: TikTokApi - @staticmethod - def users(**kwargs) -> Iterator[User]: - """ - Trending users - - Example Usage - ```py - for user in api.trending.users('therock'): - # do something - ``` - """ - return Trending.discover_type(prefix="user", **kwargs) - - @staticmethod - def sounds(**kwargs) -> Iterator[Sound]: - """ - Trending sounds - - Example Usage - ```py - for user in api.trending.sounds('funny'): - # do something - ``` - """ - return Trending.discover_type(prefix="music", **kwargs) - - @staticmethod - def hashtags(**kwargs) -> Iterator[Hashtag]: - """ - Trending hashtags - - Example Usage - ```py - for user in api.trending.hashtags('funny'): - # do something - ``` - """ - return Trending.discover_type(prefix="challenge", **kwargs) - - @staticmethod - def discover_type(prefix, count=28, offset=0, **kwargs) -> Iterator: - """ - Returns trending objects. - - You should instead use the users/sounds/hashtags as they all use data - from this function. - - - Parameters: - - search_term (str): The phrase you want to search for. - - prefix (str): either user|music|challenge - - Example Usage - ```py - for user in api.search.discover_type('therock', 'user'): - # do something - ``` - - """ - # TODO: Investigate if this is actually working as expected. Doesn't seem to be, check offset - processed = Trending.parent._process_kwargs(kwargs) - kwargs["custom_device_id"] = processed.device_id - - cursor = offset - page_size = 28 - - while cursor - offset < count: - query = { - "discoverType": 0, - "needItemList": False, - "keyWord": "f", # Keyword is a required param, but doesn't do anything anymore. - "offset": cursor, - "count": page_size, - "useRecommend": False, - "language": "en", - } - path = "node/share/discover/{}/?{}&{}".format( - prefix, Trending.parent._add_url_params(), urlencode(query) - ) - data = Trending.parent.get_data(path, **kwargs) - - for x in data.get("userInfoList", []): - yield User(data=x["user"]) - - for x in data.get("musicInfoList", []): - yield Sound(data=x["music"]) - - for x in data.get("challengeInfoList", []): - yield Hashtag(data=x["challenge"]) - - if int(data["offset"]) <= offset: - Trending.parent.logger.info( - "TikTok is not sending videos beyond this point." - ) - return - - offset = int(data["offset"]) - @staticmethod def videos(count=30, **kwargs) -> Iterator[Video]: """ @@ -132,7 +35,7 @@ def videos(count=30, **kwargs) -> Iterator[Video]: spawn = requests.head( "https://www.tiktok.com", proxies=Trending.parent._format_proxy(processed.proxy), - **Trending.parent.requests_extra_kwargs, + **Trending.parent._requests_extra_kwargs, ) ttwid = spawn.cookies["ttwid"] diff --git a/TikTokApi/api/user.py b/TikTokApi/api/user.py index 2a1b1cba..26b79b44 100644 --- a/TikTokApi/api/user.py +++ b/TikTokApi/api/user.py @@ -93,11 +93,11 @@ def info_full(self, **kwargs) -> dict: "path": "/@{}".format(quoted_username), "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", - "User-Agent": self.parent.user_agent, + "User-Agent": self.parent._user_agent, }, proxies=User.parent._format_proxy(kwargs.get("proxy", None)), cookies=User.parent._get_cookies(**kwargs), - **User.parent.requests_extra_kwargs, + **User.parent._requests_extra_kwargs, ) data = extract_tag_contents(r.text) diff --git a/TikTokApi/tiktok.py b/TikTokApi/tiktok.py index 4c830955..a917ed9f 100644 --- a/TikTokApi/tiktok.py +++ b/TikTokApi/tiktok.py @@ -120,52 +120,50 @@ def _initialize(self, logging_level=logging.WARNING, **kwargs): self.logger.setLevel(level=logging_level) # Some Instance Vars - self.executablePath = kwargs.get("executablePath", None) + self._executablePath = kwargs.get("executablePath", 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.proxy = kwargs.get("proxy", None) - self.custom_verify_fp = kwargs.get("custom_verify_fp") - self.signer_url = kwargs.get("external_signer", None) - self.request_delay = kwargs.get("request_delay", None) - self.requests_extra_kwargs = kwargs.get("requests_extra_kwargs", {}) + 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._proxy = kwargs.get("proxy", None) + self._custom_verify_fp = kwargs.get("custom_verify_fp") + self._signer_url = kwargs.get("external_signer", None) + self._request_delay = kwargs.get("request_delay", None) + self._requests_extra_kwargs = kwargs.get("requests_extra_kwargs", {}) if kwargs.get("use_test_endpoints", False): global BASE_URL BASE_URL = "https://t.tiktok.com/" if kwargs.get("generate_static_device_id", False): - self.custom_device_id = "".join( + self._custom_device_id = "".join( random.choice(string.digits) for num in range(19) ) - if self.signer_url is None: - self.browser = browser(**kwargs) - self.user_agent = self.browser.user_agent + if self._signer_url is None: + self._browser = browser(**kwargs) + self._user_agent = self._browser.user_agent try: - self.timezone_name = self.__format_new_params(self.browser.timezone_name) - self.browser_language = self.__format_new_params( - self.browser.browser_language - ) - self.width = self.browser.width - self.height = self.browser.height - self.region = self.browser.region - self.language = self.browser.language + self._timezone_name = self._browser.timezone_name + self._browser_language = self._browser.browser_language + self._width = self._browser.width + self._height = self._browser.height + self._region = self._browser.region + self._language = self._browser.language except Exception as e: self.logger.exception( "An error occurred while opening your browser, but it was ignored\n", "Are you sure you ran python -m playwright install?", ) - self.timezone_name = "" - self.browser_language = "" - self.width = "1920" - self.height = "1080" - self.region = "US" - self.language = "en" + self._timezone_name = "" + self._browser_language = "" + self._width = "1920" + self._height = "1080" + self._region = "US" + self._language = "en" def get_data(self, path, subdomain="m", **kwargs) -> dict: """Makes requests to TikTok and returns their JSON. @@ -175,15 +173,15 @@ def get_data(self, path, subdomain="m", **kwargs) -> dict: """ processed = self._process_kwargs(kwargs) kwargs["custom_device_id"] = processed.device_id - if self.request_delay is not None: - time.sleep(self.request_delay) + if self._request_delay is not None: + time.sleep(self._request_delay) - if self.proxy is not None: - proxy = self.proxy + if self._proxy is not None: + proxy = self._proxy if kwargs.get("custom_verify_fp") == None: - if self.custom_verify_fp != None: - verifyFp = self.custom_verify_fp + if self._custom_verify_fp != None: + verifyFp = self._custom_verify_fp else: verifyFp = "verify_khr3jabg_V7ucdslq_Vrw9_4KPb_AJ1b_Ks706M8zIJTq" else: @@ -194,13 +192,13 @@ def get_data(self, path, subdomain="m", **kwargs) -> dict: full_url = f"https://{subdomain}.tiktok.com/" + path - if self.signer_url is None: + if self._signer_url is None: kwargs["custom_verify_fp"] = verifyFp - verify_fp, device_id, signature, tt_params = self.browser.sign_url( + verify_fp, device_id, signature, tt_params = self._browser.sign_url( full_url, calc_tt_params=send_tt_params, **kwargs ) - user_agent = self.browser.user_agent - referrer = self.browser.referrer + user_agent = self._browser.user_agent + referrer = self._browser.referrer else: ( verify_fp, @@ -224,7 +222,7 @@ def get_data(self, path, subdomain="m", **kwargs) -> dict: url, headers={"x-secsdk-csrf-version": "1.2.5", "x-secsdk-csrf-request": "1"}, proxies=self._format_proxy(processed.proxy), - **self.requests_extra_kwargs, + **self._requests_extra_kwargs, ) csrf_token = None @@ -258,14 +256,14 @@ def get_data(self, path, subdomain="m", **kwargs) -> dict: headers=headers, cookies=self._get_cookies(**kwargs), proxies=self._format_proxy(processed.proxy), - **self.requests_extra_kwargs, + **self._requests_extra_kwargs, ) try: - json = r.json() + parsed_data = r.json() if ( - json.get("type") == "verify" - or json.get("verifyConfig", {}).get("type", "") == "verify" + parsed_data.get("type") == "verify" + or parsed_data.get("verifyConfig", {}).get("type", "") == "verify" ): self.logger.error( "Tiktok wants to display a captcha.\nResponse:\n%s\nCookies:\n%s", @@ -315,7 +313,7 @@ def get_data(self, path, subdomain="m", **kwargs) -> dict: "10404": "FYP_VIDEO_LIST_LIMIT", "undefined": "MEDIA_ERROR", } - statusCode = json.get("statusCode", 0) + statusCode = parsed_data.get("statusCode", 0) self.logger.info(f"TikTok Returned: %s", json) if statusCode == 10201: # Invalid Entity @@ -347,7 +345,7 @@ def get_data(self, path, subdomain="m", **kwargs) -> dict: def __del__(self): """A basic cleanup method, called automatically from the code""" try: - self.browser._clean_up() + self._browser._clean_up() except Exception: pass try: @@ -382,8 +380,8 @@ def external_signer(self, url, custom_device_id=None, verifyFp=None): else: query = {"url": url, "verifyFp": verifyFp} data = requests.get( - self.signer_url + "?{}".format(urlencode(query)), - **self.requests_extra_kwargs, + self._signer_url + "?{}".format(urlencode(query)), + **self._requests_extra_kwargs, ) parsed_data = data.json() @@ -402,8 +400,8 @@ def _get_cookies(self, **kwargs): "".join(random.choice(string.digits) for num in range(19)), ) if kwargs.get("custom_verify_fp") is None: - if self.custom_verify_fp is not None: - verifyFp = self.custom_verify_fp + if self._custom_verify_fp is not None: + verifyFp = self._custom_verify_fp else: verifyFp = None else: @@ -437,12 +435,12 @@ def get_bytes(self, **kwargs) -> bytes: """Returns TikTok's response as bytes, similar to get_data""" processed = self._process_kwargs(kwargs) kwargs["custom_device_id"] = processed.device_id - if self.signer_url is None: - verify_fp, device_id, signature, _ = self.browser.sign_url( + if self._signer_url is None: + verify_fp, device_id, signature, _ = self._browser.sign_url( calc_tt_params=False, **kwargs ) - user_agent = self.browser.user_agent - referrer = self.browser.referrer + user_agent = self._browser.user_agent + referrer = self._browser.referrer else: ( verify_fp, @@ -487,8 +485,8 @@ def _format_proxy(self, proxy) -> Optional[dict]: """ Formats the proxy object """ - if proxy is None and self.proxy is not None: - proxy = self.proxy + if proxy is None and self._proxy is not None: + proxy = self._proxy if proxy is not None: return {"http": proxy, "https": proxy} else: @@ -496,7 +494,6 @@ def _format_proxy(self, proxy) -> Optional[dict]: # Process the kwargs def _process_kwargs(self, kwargs): - # TODO: Just return this as a dict, then only use the keys you need region = kwargs.get("region", "US") language = kwargs.get("language", "en") proxy = kwargs.get("proxy", None) @@ -504,8 +501,8 @@ def _process_kwargs(self, kwargs): if kwargs.get("custom_device_id", None) != None: device_id = kwargs.get("custom_device_id") else: - if self.custom_device_id != None: - device_id = self.custom_device_id + if self._custom_device_id != None: + device_id = self._custom_device_id else: device_id = "".join(random.choice(string.digits) for num in range(19)) @@ -524,36 +521,32 @@ def __get_js(self, proxy=None) -> str: return requests.get( "https://sf16-muse-va.ibytedtos.com/obj/rc-web-sdk-gcs/acrawler.js", proxies=self._format_proxy(proxy), - **self.requests_extra_kwargs, + **self._requests_extra_kwargs, ).text - def __format_new_params(self, parm) -> str: - # TODO: Maybe try not doing this? It should be handled by the urlencode. - return parm.replace("/", "%2F").replace(" ", "+").replace(";", "%3B") - def _add_url_params(self) -> str: query = { "aid": 1988, "app_name": "tiktok_web", "device_platform": "web_mobile", - "region": self.region or "US", + "region": self._region or "US", "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", + "screen_width": self._width, + "screen_height": self._height, + "browser_language": self._browser_language.lower() or "en-us", "browser_platform": "iPhone", "browser_name": "Mozilla", - "browser_version": self.user_agent, + "browser_version": self._user_agent, "browser_online": "true", - "timezone_name": self.timezone_name or "America/Chicago", + "timezone_name": self._timezone_name or "America/Chicago", "is_page_visible": "true", "focus_state": "true", "is_fullscreen": "false", "history_len": random.randint(0, 30), - "language": self.language or "en", + "language": self._language or "en", } return urlencode(query) diff --git a/tests/test_search.py b/tests/test_search.py index 1174e165..9d00989c 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -6,11 +6,7 @@ def test_video(): count = 0 - found_ids = [] for video in api.search.videos("therock", count=50): - if video.id in found_ids: - raise Exception("Duplicate Video on search.videos") - found_ids.append(video.id) count += 1 assert count >= 50 diff --git a/tests/test_trending.py b/tests/test_trending.py index ea26e13b..0df95e47 100644 --- a/tests/test_trending.py +++ b/tests/test_trending.py @@ -9,36 +9,4 @@ def test_trending_videos(): for video in api.trending.videos(count=100): count += 1 - assert count >= 100 - - -def test_discover_type(): - count = 0 - found_ids = [] - for result in api.trending.users(): - if result.user_id in found_ids: - raise Exception("Multiple Users on trending.user") - found_ids.append(result.user_id) - count += 1 - - assert count >= 50 - - count = 0 - found_ids - for result in api.trending.sounds(): - if result.id in found_ids: - raise Exception("Multiple Sounds on trending.sound") - found_ids.append(result.id) - count += 1 - - assert count >= 50 - - count = 0 - found_ids = [] - for result in api.trending.hashtags(): - if result.id in found_ids: - raise Exception("Multiple Users on trending.user") - found_ids.append(result.id) - count += 1 - - assert count >= 50 + assert count >= 100 \ No newline at end of file From 46c964fcb3a936cbfec25a8331417bac9d25d63e Mon Sep 17 00:00:00 2001 From: David Teather <34144122+davidteather@users.noreply.github.com> Date: Sun, 13 Feb 2022 00:19:42 -0600 Subject: [PATCH 17/17] Yeahhhh --- CITATION.cff | 2 +- TikTokApi/api/__init__.py | 14 + TikTokApi/api/hashtag.py | 2 +- TikTokApi/api/search.py | 2 +- TikTokApi/api/sound.py | 4 +- TikTokApi/api/user.py | 11 +- TikTokApi/browser_utilities/browser.py | 6 +- TikTokApi/tiktok.py | 47 +- docs/TikTokApi.html | 6 +- docs/TikTokApi/api.html | 57 +- docs/TikTokApi/api/hashtag.html | 76 +- docs/TikTokApi/api/search.html | 556 +++--------- docs/TikTokApi/api/sound.html | 78 +- docs/TikTokApi/api/trending.html | 69 +- docs/TikTokApi/api/user.html | 114 +-- docs/TikTokApi/api/video.html | 66 +- docs/TikTokApi/browser_utilities.html | 6 +- docs/TikTokApi/browser_utilities/browser.html | 24 +- .../browser_utilities/browser_interface.html | 6 +- .../browser_utilities/get_acrawler.html | 6 +- docs/TikTokApi/exceptions.html | 6 +- docs/TikTokApi/helpers.html | 6 +- docs/TikTokApi/tiktok.html | 854 ++++++++---------- docs/TikTokApi/utilities.html | 6 +- docs/search.js | 2 +- setup.py | 2 +- tests/test_trending.py | 2 +- 27 files changed, 764 insertions(+), 1266 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index 554b47bf..79e7ddea 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -6,4 +6,4 @@ authors: title: "TikTokAPI" url: "https://github.com/davidteather/tiktok-api" version: 5.0.0 -date-released: 2022-1-31 +date-released: 2022-2-11 diff --git a/TikTokApi/api/__init__.py b/TikTokApi/api/__init__.py index 48ff818a..d75df9bd 100644 --- a/TikTokApi/api/__init__.py +++ b/TikTokApi/api/__init__.py @@ -2,4 +2,18 @@ This module contains classes that all represent different types of data sent back by the TikTok servers. The files within in module correspond to what type of object is described and all have different methods associated with them. + + +### How To Interpret TikTok Data +There are quite a few ambigious keys in the JSON that TikTok returns so here's a section that tries to document some of them. + +**Note**: These are incomplete, if you get confused about something feel free to add it here as a PR once you figure it out. + +| JSON Key | Description | +|------------------|-------------| +| createTime | The [unix epoch](https://docs.python.org/3/library/datetime.html#datetime.date.fromtimestamp) of creation, all other time fields are also unix epochs. | +| secUid & (userId or id) | Two different unique attributes that are used in conjunction to reference a specific account, so if you're storing users somewhere in a database, you should store both secUid & userId. | +| id | A unique attribute used to reference a non-user object like video, hashtag, etc | +| diggCount | The likes for a specific video. | +| digged | Used to check if the current user has liked/digged a video, this will always be false since this package doesn't support logged-in user functions. | """ diff --git a/TikTokApi/api/hashtag.py b/TikTokApi/api/hashtag.py index 054391b6..aba2fad4 100644 --- a/TikTokApi/api/hashtag.py +++ b/TikTokApi/api/hashtag.py @@ -88,7 +88,7 @@ def videos(self, count=30, offset=0, **kwargs) -> Iterator[Video]: - Parameters: - count (int): The amount of videos you want returned. - - cursor (int): The unix epoch to get videos since. TODO: Check this is right + - offset (int): The the offset of videos from 0 you want to get. Example Usage ```py diff --git a/TikTokApi/api/search.py b/TikTokApi/api/search.py index 492ddbe0..6a70f219 100644 --- a/TikTokApi/api/search.py +++ b/TikTokApi/api/search.py @@ -28,7 +28,7 @@ def videos(search_term, count=28, offset=0, **kwargs) -> Iterator[Video]: - Parameters: - search_term (str): The phrase you want to search for. - count (int): The amount of videos you want returned. - TODO: Figure out offset + - offset (int): The offset of videos from your data you want returned. Example Usage ```py diff --git a/TikTokApi/api/sound.py b/TikTokApi/api/sound.py index 66e2dc3a..69770c25 100644 --- a/TikTokApi/api/sound.py +++ b/TikTokApi/api/sound.py @@ -95,7 +95,7 @@ def info_full(self, **kwargs) -> dict: "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate", "Connection": "keep-alive", - "User-Agent": self.parent.user_agent, + "User-Agent": self.parent._user_agent, }, proxies=self.parent._format_proxy(kwargs.get("proxy", None)), cookies=self.parent._get_cookies(**kwargs), @@ -111,7 +111,7 @@ def videos(self, count=30, offset=0, **kwargs) -> Iterator[Video]: - Parameters: - count (int): The amount of videos you want returned. - - cursor (int): The unix epoch to get videos since. TODO: Check this is right + - offset (int): The offset of videos you want returned. Example Usage ```py diff --git a/TikTokApi/api/user.py b/TikTokApi/api/user.py index 26b79b44..35a09565 100644 --- a/TikTokApi/api/user.py +++ b/TikTokApi/api/user.py @@ -117,13 +117,13 @@ def videos(self, count=30, cursor=0, **kwargs) -> Iterator[Video]: - Parameters: - count (int): The amount of videos you want returned. - - cursor (int): The unix epoch to get videos since. TODO: Check this is right + - cursor (int): The unix epoch to get uploaded videos since. Example Usage ```py user = api.user(username='therock') for video in user.videos(count=100): - print(video.id) + # do something ``` """ processed = User.parent._process_kwargs(kwargs) @@ -176,12 +176,12 @@ def liked(self, count: int = 30, cursor: int = 0, **kwargs) -> Iterator[Video]: - Parameters: - count (int): The amount of videos you want returned. - - cursor (int): The unix epoch to get videos since. TODO: Check this is right + - cursor (int): The unix epoch to get uploaded videos since. Example Usage ```py for liked_video in api.user(username='public_likes'): - print(liked_video.id) + # do something ``` """ processed = User.parent._process_kwargs(kwargs) @@ -213,7 +213,8 @@ def liked(self, count: int = 30, cursor: int = 0, **kwargs) -> Iterator[Video]: res = self.parent.get_data(path, **kwargs) if "itemList" not in res.keys(): - User.parent.logger.error("User's likes are most likely private") + if first: + User.parent.logger.error("User's likes are most likely private") return videos = res.get("itemList", []) diff --git a/TikTokApi/browser_utilities/browser.py b/TikTokApi/browser_utilities/browser.py index 2e43d027..b0982c7e 100644 --- a/TikTokApi/browser_utilities/browser.py +++ b/TikTokApi/browser_utilities/browser.py @@ -41,7 +41,7 @@ def __init__( self.api_url = kwargs.get("api_url", None) self.referrer = kwargs.get("referer", "https://www.tiktok.com/") self.language = kwargs.get("language", "en") - self.executablePath = kwargs.get("executablePath", None) + self.executable_path = kwargs.get("executable_path", None) self.device_id = kwargs.get("custom_device_id", None) args = kwargs.get("browser_args", []) @@ -73,8 +73,8 @@ def __init__( self.options.update(options) - if self.executablePath is not None: - self.options["executablePath"] = self.executablePath + if self.executable_path is not None: + self.options["executable_path"] = self.executable_path try: self.browser = get_playwright().webkit.launch( diff --git a/TikTokApi/tiktok.py b/TikTokApi/tiktok.py index a917ed9f..3b3460f2 100644 --- a/TikTokApi/tiktok.py +++ b/TikTokApi/tiktok.py @@ -5,6 +5,7 @@ import string import time from typing import ClassVar, Optional +from urllib import request from urllib.parse import quote, urlencode import requests @@ -40,7 +41,19 @@ class TikTokApi: trending = Trending @staticmethod - def __new__(cls, logging_level=logging.WARNING, *args, **kwargs): + def __new__( + cls, + logging_level=logging.WARNING, + request_delay: Optional[int] = None, + custom_device_id: Optional[str] = None, + generate_static_device_id: Optional[bool] = False, + custom_verify_fp: Optional[str] = None, + use_test_endpoints: Optional[bool] = False, + proxy: Optional[str] = None, + executable_path: Optional[str] = None, + *args, + **kwargs, + ): """The TikTokApi class. Used to interact with TikTok. This is a singleton class to prevent issues from arising with playwright @@ -89,12 +102,7 @@ class to prevent issues from arising with playwright to store this at the instance level. You can override this at the specific methods. - * use_selenium: Option to use selenium over playwright, optional - Playwright is selected by default and is the one that I'm designing the - package to be compatable for, however if playwright doesn't work on - your machine feel free to set this to True. - - * executablePath: The location of the driver, optional + * executable_path: The location of the driver, optional This shouldn't be needed if you're using playwright * **kwargs @@ -105,7 +113,18 @@ class to prevent issues from arising with playwright if cls._instance is None: cls._instance = super(TikTokApi, cls).__new__(cls) - cls._instance._initialize(logging_level=logging_level, *args, **kwargs) + cls._instance._initialize( + logging_level=logging_level, + request_delay=request_delay, + custom_device_id=custom_device_id, + generate_static_device_id=generate_static_device_id, + custom_verify_fp=custom_verify_fp, + use_test_endpoints=use_test_endpoints, + proxy=proxy, + executable_path=executable_path, + *args, + **kwargs, + ) return cls._instance def _initialize(self, logging_level=logging.WARNING, **kwargs): @@ -120,7 +139,7 @@ def _initialize(self, logging_level=logging.WARNING, **kwargs): self.logger.setLevel(level=logging_level) # Some Instance Vars - self._executablePath = kwargs.get("executablePath", None) + 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'") @@ -266,9 +285,10 @@ def get_data(self, path, subdomain="m", **kwargs) -> dict: or parsed_data.get("verifyConfig", {}).get("type", "") == "verify" ): self.logger.error( - "Tiktok wants to display a captcha.\nResponse:\n%s\nCookies:\n%s", + "Tiktok wants to display a captcha.\nResponse:\n%s\nCookies:\n%s\nURL:\n%s", r.text, self._get_cookies(**kwargs), + url, ) raise TikTokCaptchaError() @@ -517,13 +537,6 @@ class ProcessedKwargs: region=region, language=language, proxy=proxy, device_id=device_id ) - def __get_js(self, proxy=None) -> str: - return requests.get( - "https://sf16-muse-va.ibytedtos.com/obj/rc-web-sdk-gcs/acrawler.js", - proxies=self._format_proxy(proxy), - **self._requests_extra_kwargs, - ).text - def _add_url_params(self) -> str: query = { "aid": 1988, diff --git a/docs/TikTokApi.html b/docs/TikTokApi.html index 16d367ee..1bd7472b 100644 --- a/docs/TikTokApi.html +++ b/docs/TikTokApi.html @@ -3,18 +3,18 @@ - + TikTokApi API documentation - +