diff --git a/.sphinx/TikTokLive.client.rst b/.sphinx/TikTokLive.client.rst index 08e2d24..e6a5c1b 100644 --- a/.sphinx/TikTokLive.client.rst +++ b/.sphinx/TikTokLive.client.rst @@ -20,6 +20,14 @@ TikTokLive.client.client module :undoc-members: :show-inheritance: +TikTokLive.client.config module +------------------------------- + +.. automodule:: TikTokLive.client.config + :members: + :undoc-members: + :show-inheritance: + TikTokLive.client.http module ----------------------------- @@ -28,10 +36,10 @@ TikTokLive.client.http module :undoc-members: :show-inheritance: -TikTokLive.client.proxy module ------------------------------- +TikTokLive.client.websocket module +---------------------------------- -.. automodule:: TikTokLive.client.proxy +.. automodule:: TikTokLive.client.websocket :members: :undoc-members: :show-inheritance: diff --git a/.sphinx/_build/doctrees/README.doctree b/.sphinx/_build/doctrees/README.doctree index fa04cc1..b6f8f8e 100644 Binary files a/.sphinx/_build/doctrees/README.doctree and b/.sphinx/_build/doctrees/README.doctree differ diff --git a/.sphinx/_build/doctrees/TikTokLive.client.doctree b/.sphinx/_build/doctrees/TikTokLive.client.doctree index dfbd3f3..be40dc3 100644 Binary files a/.sphinx/_build/doctrees/TikTokLive.client.doctree and b/.sphinx/_build/doctrees/TikTokLive.client.doctree differ diff --git a/.sphinx/_build/doctrees/TikTokLive.doctree b/.sphinx/_build/doctrees/TikTokLive.doctree index 57d29ce..478a94c 100644 Binary files a/.sphinx/_build/doctrees/TikTokLive.doctree and b/.sphinx/_build/doctrees/TikTokLive.doctree differ diff --git a/.sphinx/_build/doctrees/TikTokLive.proto.doctree b/.sphinx/_build/doctrees/TikTokLive.proto.doctree index c5c7a3b..11e306d 100644 Binary files a/.sphinx/_build/doctrees/TikTokLive.proto.doctree and b/.sphinx/_build/doctrees/TikTokLive.proto.doctree differ diff --git a/.sphinx/_build/doctrees/TikTokLive.types.doctree b/.sphinx/_build/doctrees/TikTokLive.types.doctree index 6e3a44b..dceae0b 100644 Binary files a/.sphinx/_build/doctrees/TikTokLive.types.doctree and b/.sphinx/_build/doctrees/TikTokLive.types.doctree differ diff --git a/.sphinx/_build/doctrees/environment.pickle b/.sphinx/_build/doctrees/environment.pickle index 1d90d56..344f2d9 100644 Binary files a/.sphinx/_build/doctrees/environment.pickle and b/.sphinx/_build/doctrees/environment.pickle differ diff --git a/.sphinx/_build/doctrees/index.doctree b/.sphinx/_build/doctrees/index.doctree index d241107..6f93b28 100644 Binary files a/.sphinx/_build/doctrees/index.doctree and b/.sphinx/_build/doctrees/index.doctree differ diff --git a/.sphinx/_build/doctrees/modules.doctree b/.sphinx/_build/doctrees/modules.doctree index 258b706..2782dc2 100644 Binary files a/.sphinx/_build/doctrees/modules.doctree and b/.sphinx/_build/doctrees/modules.doctree differ diff --git a/README.md b/README.md index 2afdc7c..2745b7b 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ -TikTokLive (Note: Issue with connecting known, being worked on) +TikTokLive ================== A python library to connect to and read events from TikTok's LIVE service [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white&style=flat-square)](https://www.linkedin.com/in/isaac-kogan-5a45b9193/ ) [![HitCount](https://hits.dwyl.com/isaackogan/TikTokLive.svg?style=flat)](http://hits.dwyl.com/isaackogan/TikTokLive) -![Downloads](https://pepy.tech/badge/tiktoklive) -![Issues](https://img.shields.io/github/issues/isaackogan/TikTok-Live-Connector) -![Forks](https://img.shields.io/github/forks/isaackogan/TikTok-Live-Connector) -![Stars](https://img.shields.io/github/stars/isaackogan/TikTok-Live-Connector) +![Downloads](https://pepy.tech/badge/TikTokLive) +![Issues](https://img.shields.io/github/issues/isaackogan/TikTokLive) +![Forks](https://img.shields.io/github/forks/isaackogan/TikTokLive) +![Stars](https://img.shields.io/github/stars/isaackogan/TikTokLive) [![Support Server](https://img.shields.io/discord/977648006063091742.svg?color=7289da&logo=discord&style=flat-square)](https://discord.gg/e2XwPNTBBr) -A python library to receive and decode livestream events such as comments and gifts in real-time from TikTok's LIVE service by connecting to TikTok's internal WebCast push service. This library includes a wrapper that -connects to the WebCast service using only a user's `unique_id` and allows you to join your livestream as well as that of other streamers. No credentials are required to use TikTokLive. +A python library to receive and decode livestream events such as comments and gifts in real-time from TikTok's LIVE service by connecting to TikTok's internal Webcast push service. This library includes a wrapper that +connects to the Webcast service using only a user's `unique_id` and allows you to join your livestream as well as that of other streamers. No credentials are required to use TikTokLive. This library a Python implementation of the Javascript [TikTok-Live-Connector](https://github.com/zerodytrash/TikTok-Live-Connector) @@ -27,45 +27,40 @@ Join the [support discord](https://discord.gg/e2XwPNTBBr) and visit the `#suppor **Primary Information** -- [Documentation](https://tiktoklive.isaackogan.com) +- [Documentation](https://isaackogan.github.io/TikTokLive/) - [Contributors](#contributors) - [License](#license) -- [Thermal Printing](#-thermal-printing-library-) +- [Thermal Printing](#thermal-printing-library-for-sale-) **Resources & Guides** -1. [David's Intro Tutorial](#intro-tutorial) +1. [David's Intro Tutorial](#tiktoklive-intro-tutorial) 2. [Getting Started](#getting-started) 3. [Params & Options](#Params-&-Options) 4. [Client Methods](#Methods) -5. [TikTok Events](#Events) -6. [Usage Examples](https://github.com/davidteather/TikTok-Api/tree/master/examples) +5. [Client Attributes](#Attributes) +6. [TikTok Events](#Events) +7. [Usage Examples](https://github.com/davidteather/TikTok-Api/tree/master/examples) -## 💲🖨 Thermal Printing Library 🖨💲 +## Thermal Printing Library for Sale 🖨 -Thermal printing is a very recent, very exciting trend on TikTok. +Print text, images, text-to-speech, play sounds, and much more. It's just a one-time, life-time purchase, including future support and updates. -I developed an all-encompassing, multithreaded thermal printing program that does *everything* -you could ever want with Thermal Printing. It even has its own [YouTube Tutorial](https://www.youtube.com/watch?v=NeapS5Jn_oo) and comes -with [pre-made examples](https://github.com/isaackogan/TikTokPrinter/tree/master/examples) if your coding ability isn't very strong! +Install is easy, takes about 15 minutes. Plug your printer in, download the drivers, and run a command in terminal. -Print text, images, text-to-speech, play sounds, and much more. There is no subscription unlike virtual printer services. It's just a one-time, life-time purchase. - -It's so easy, it can be installed in **one** command through pip. It's not just a "one-off" purchase, either. As the project is updated, you will have access to **every new release** as new features are added. +As the project is updated, you will have access to **every new release** as new features are added. Here's a sample of what you can do with this library in less than 30 lines of code: +It even has its own [YouTube Tutorial](https://www.youtube.com/watch?v=NeapS5Jn_oo) and comes with [pre-made examples](https://github.com/isaackogan/TikTokPrinter/tree/master/examples) if your coding ability isn't very +strong! [![](https://github.com/isaackogan/TikTokLive/raw/master/.github/RESOURCES/printer.gif)](https://github.com/isaackogan/TikTokPrinter) ### How to Purchase -First, read more about it on the public [TikTokPrinter](https://github.com/isaackogan/TikTokPrinter) GitHub page. - -Then, to buy this library, create a ticket in the `#tickets` channel in https://discord.gg/4Mbw58w5Qx. +To buy this library, create a ticket in the `#tickets` channel in https://discord.gg/4Mbw58w5Qx. -Type "Printer Magic" in the ticket to get started with your purchase. - -## Intro Tutorial +## TikTokLive Intro Tutorial I cannot recommend this tutorial enough for people trying to get started. It is succinct, informative and easy to understand, created by [David Teather](https://github.com/davidteather), the creator of the Python [TikTok-Api](https://github.com/davidteather/TikTok-Api) package. Click the thumbnail to warp. @@ -132,17 +127,9 @@ from TikTokLive import TikTokLiveClient client: TikTokLiveClient = TikTokLiveClient( unique_id="@oldskoldj", **( { - # Whether to process initial data (cached chats, etc.) - "process_initial_data": True, - - # Connect info (viewers, stream status, etc.) - "fetch_room_info_on_connect": True, - # Whether to get extended gift info (Image URLs, etc.) - "enable_extended_gift_info": True, - - # How frequently to poll Webcast API - "polling_interval_ms": 1000, + # Custom Asyncio event loop + "loop": None, # Custom Client params "client_params": {}, @@ -153,23 +140,39 @@ client: TikTokLiveClient = TikTokLiveClient( # Custom timeout for Webcast API requests "timeout_ms": 1000, - # Custom Asyncio event loop - "loop": None, + # How frequently to poll Webcast API + "ping_interval_ms": 1000, + + # Whether to process initial data (cached chats, etc.) + "process_initial_data": True, + + # Whether to get extended gift info (Image URLs, etc.) + "enable_extended_gift_info": True, - # Whether to trust environment variables that provide proxies to be used in aiohttp requests + # Whether to trust environment variables that provide proxies to be used in http requests "trust_env": False, - # A ProxyContainer object for proxied requests - "proxy_container": None, + # A dict object for proxies requests + "proxies": { + "http://": "http://username:password@localhost:8030", + "https://": "http://420.69.420:8031", + }, # Set the language for Webcast responses (Changes extended_gift's language) - "lang": "en-US" + "lang": "en-US", + + # Connect info (viewers, stream status, etc.) + "fetch_room_info_on_connect": True, + + # Whether to allow Websocket connections + "websocket_enabled": False } ) ) -client.run() +if __name__ == "__main__": + client.run() ``` ## Methods @@ -184,12 +187,22 @@ A `TikTokLiveClient` object contains the following methods. | retrieve_room_info | Gets the current room info from TikTok API | | retrieve_available_gifts | Retrieves a list of the available gifts for the room and adds it to the `extended_gift` attribute of the `Gift` object on the `gift` event, when enabled. | | add_listener | Adds an *asynchronous* listener function (or, you can decorate a function with `@client.on()`) and takes two parameters, an event name and the payload, an AbstractEvent || -| add_proxies | Add proxies to the current list of proxies with a valid aiohttp proxy-url | -| get_proxies | Get the current list of proxies by proxy-url | -| remove_proxies | Remove proxies from the current list of proxies by proxy-url | -| set_proxies_enabled | Set whether or not proxies are enabled (disabled by default) | | download | Start downloading the livestream video for a given duration or until stopped via the `stop_download` method | | stop_download | Stop downloading the livestream video if currently downloading, otherwise throws an error | +| send_message | Send a message to the TikTok LIVE chat using session cookies | +| set_proxies | Set proxies to be used in HTTP requests (excludes the Websocket connection) | +| get_proxies | Get the current proxies being used for requests | + +## Attributes + +| Attribute Name | Description | +|-----------------|-----------------------------------------------------------------------------------------| +| viewer_count | The number of people currently watching the livestream broadcast | +| room_id | The ID of the livestream room the client is currently connected to | +| room_info | Information about the given livestream room | +| unique_id | The TikTok username of the person whose livestream the client is currently connected to | +| connected | Whether the client is currently connected to a livestream | +| available_gifts | A dictionary containing K:V pairs of `Dict[int, ExtendedGift]` | | ## Events @@ -399,11 +412,11 @@ async def on_connect(event: UnknownEvent): ### `error` -Triggered when there is an error in the client or in error handlers. +Triggered when there is an error in the client or error handlers. If this handler is not present in the code, an internal default handler will log errors in the console. If a handler is added, all error handling (including logging) is up to the individual. -**Warning:** If you listen for the error event and do not log errors, you will not see when an error occurs. +**Warning:** If you listen for the error event and do not log errors, you will not see when an error occurs. This is because listening to the error event causes the default one to be overriden/turned off. ```python @@ -422,7 +435,7 @@ async def on_connect(error: Exception): ## Contributors * **Isaac Kogan** - *Initial work & primary maintainer* - [isaackogan](https://github.com/isaackogan) -* **Zerody** - *Reverse-Engineering & README.md file* - [Zerody](https://github.com/zerodytrash/) +* **Zerody** - *Reverse-Engineering & Support* - [Zerody](https://github.com/zerodytrash/) * **Davincible** - *Reverse-Engineering Stream Downloads* - [davincible](https://github.com/davincible) * **David Teather** - *TikTokLive Introduction Tutorial* - [davidteather](https://github.com/davidteather) diff --git a/TikTokLive/client/base.py b/TikTokLive/client/base.py index 227da0a..38d1ffe 100644 --- a/TikTokLive/client/base.py +++ b/TikTokLive/client/base.py @@ -11,15 +11,17 @@ from dacite import from_dict from ffmpy import FFmpeg, FFRuntimeError +from pyee import AsyncIOEventEmitter +from TikTokLive.client import config from TikTokLive.client.http import TikTokHTTPClient -from TikTokLive.client.proxy import ProxyContainer +from TikTokLive.client.websocket import WebcastWebsocket from TikTokLive.types import AlreadyConnecting, AlreadyConnected, LiveNotFound, FailedConnection, ExtendedGift, InvalidSessionId, ChatMessageSendFailure, ChatMessageRepeat, FailedFetchRoomInfo, FailedFetchGifts, \ - FailedRoomPolling, FFmpegWrapper, AlreadyDownloadingStream, DownloadProcessNotFound, NotDownloadingStream + FailedRoomPolling, FFmpegWrapper, AlreadyDownloadingStream, DownloadProcessNotFound, NotDownloadingStream, InitialCursorMissing from TikTokLive.utils import validate_and_normalize_unique_id, get_room_id_from_main_page_html -class BaseClient: +class BaseClient(AsyncIOEventEmitter): """ Base client responsible for long polling to the TikTok Webcast API @@ -32,13 +34,14 @@ def __init__( client_params: Optional[dict] = None, headers: Optional[dict] = None, timeout_ms: Optional[int] = None, - polling_interval_ms: int = 1000, + ping_interval_ms: int = 1000, process_initial_data: bool = True, - fetch_room_info_on_connect: bool = True, enable_extended_gift_info: bool = True, trust_env: bool = False, - proxy_container: Optional[ProxyContainer] = None, - lang: Optional[str] = "en-US" + proxies: Optional[Dict[str, str]] = None, + lang: Optional[str] = "en-US", + fetch_room_info_on_connect: bool = True, + websocket_enabled: bool = True, ): """ Initialize the base client @@ -48,15 +51,17 @@ def __init__( :param client_params: Additional client parameters to include when making requests to the Webcast API :param headers: Additional headers to include when making requests to the Webcast API :param timeout_ms: The timeout (in ms) for requests made to the Webcast API - :param polling_interval_ms: The interval between requests made to the Webcast API + :param ping_interval_ms: The interval between requests made to the Webcast API for both Websockets and Long Polling :param process_initial_data: Whether to process the initial data (including cached chats) - :param fetch_room_info_on_connect: Whether to fetch room info (check if everything is kosher) on connect :param enable_extended_gift_info: Whether to retrieve extended gift info including its icon & other important things :param trust_env: Whether to trust environment variables that provide proxies to be used in aiohttp requests - :param proxy_container: A proxy container that allows you to submit an unlimited # of proxies for rotation + :param proxies: Enable proxied requests by turning on forwarding for the HTTPX "proxies" argument. Websocket connections will NOT be proxied :param lang: Change the language. Payloads *will* be in English, but this will change stuff like the extended_gift Gift attribute to the desired language! + :param fetch_room_info_on_connect: Whether to fetch room info on connect. If disabled, you might attempt to connect to a closed livestream + :param websocket_enabled: Whether to use websockets or rely on purely long polling """ + AsyncIOEventEmitter.__init__(self) # Get Event Loop if isinstance(loop, AbstractEventLoop): @@ -77,19 +82,31 @@ def __init__( self.__connecting: bool = False self.__connected: bool = False self.__session_id: Optional[str] = None + self.__is_ws_upgrade_done: bool = False + self.__websocket_enabled: bool = websocket_enabled # Change Language - TikTokHTTPClient.DEFAULT_CLIENT_PARAMS["app_language"] = lang - TikTokHTTPClient.DEFAULT_CLIENT_PARAMS["webcast_language"] = lang + config.DEFAULT_CLIENT_PARAMS["app_language"] = lang + config.DEFAULT_CLIENT_PARAMS["webcast_language"] = lang # Protected Attributes - self._client_params: dict = {**TikTokHTTPClient.DEFAULT_CLIENT_PARAMS, **(client_params if isinstance(client_params, dict) else dict())} - self._http: TikTokHTTPClient = TikTokHTTPClient(headers if headers is not None else dict(), timeout_ms=timeout_ms, proxy_container=proxy_container, trust_env=trust_env) - self._polling_interval_ms: int = polling_interval_ms + + self._http: TikTokHTTPClient = TikTokHTTPClient( + headers=headers if headers is not None else dict(), + timeout_ms=timeout_ms, + proxies=proxies, + trust_env=trust_env, + params={**config.DEFAULT_CLIENT_PARAMS, **(client_params if isinstance(client_params, dict) else dict())} + ) + self._ping_interval_ms: int = ping_interval_ms self._process_initial_data: bool = process_initial_data - self._fetch_room_info_on_connect: bool = fetch_room_info_on_connect self._enable_extended_gift_info: bool = enable_extended_gift_info + self._fetch_room_info_on_connect: bool = fetch_room_info_on_connect self._download: Optional[FFmpegWrapper] = None + self._socket: Optional[WebcastWebsocket] = None + + # Listeners + self.add_listener("websocket", self._handle_webcast_messages) async def _on_error(self, original: Exception, append: Optional[Exception]) -> None: """ @@ -115,10 +132,10 @@ async def __fetch_room_id(self) -> Optional[str]: try: html: str = await self._http.get_livestream_page_html(self.__unique_id) self.__room_id = get_room_id_from_main_page_html(html) - self._client_params["room_id"] = self.__room_id + self._http.params["room_id"] = self.__room_id return self.__room_id except Exception as ex: - await self._on_error(ex, FailedFetchRoomInfo("Failed to fetch room id from WebCast, see stacktrace for more info.")) + await self._on_error(ex, FailedFetchRoomInfo("Failed to fetch room id from Webcast, see stacktrace for more info.")) return None async def __fetch_room_info(self) -> Optional[dict]: @@ -130,11 +147,11 @@ async def __fetch_room_info(self) -> Optional[dict]: """ try: - response = await self._http.get_json_object_from_webcast_api("room/info/", self._client_params) + response = await self._http.get_json_object_from_webcast_api("room/info/", self._http.params) self.__room_info = response return self.__room_info except Exception as ex: - await self._on_error(ex, FailedFetchRoomInfo("Failed to fetch room info from WebCast, see stacktrace for more info.")) + await self._on_error(ex, FailedFetchRoomInfo("Failed to fetch room info from Webcast, see stacktrace for more info.")) return None async def __fetch_available_gifts(self) -> Optional[Dict[int, ExtendedGift]]: @@ -146,7 +163,7 @@ async def __fetch_available_gifts(self) -> Optional[Dict[int, ExtendedGift]]: """ try: - response = await self._http.get_json_object_from_webcast_api("gift/list/", self._client_params) + response = await self._http.get_json_object_from_webcast_api("gift/list/", self._http.params) gifts: Optional[List] = response.get("gifts") if isinstance(gifts, list): @@ -159,7 +176,7 @@ async def __fetch_available_gifts(self) -> Optional[Dict[int, ExtendedGift]]: return self.__available_gifts except Exception as ex: - await self._on_error(ex, FailedFetchGifts("Failed to fetch gift data from WebCast, see stacktrace for more info.")) + await self._on_error(ex, FailedFetchGifts("Failed to fetch gift data from Webcast, see stacktrace for more info.")) return None async def __fetch_room_polling(self) -> None: @@ -171,13 +188,13 @@ async def __fetch_room_polling(self) -> None: """ self.__is_polling_enabled = True - polling_interval: int = int(self._polling_interval_ms / 1000) + polling_interval: float = self._ping_interval_ms / 1000 while self.__is_polling_enabled: try: await self.__fetch_room_data() except Exception as ex: - await self._on_error(ex, FailedRoomPolling("Failed to retrieve events from WebCast, see stacktrace for more info.")) + await self._on_error(ex, FailedRoomPolling("Failed to retrieve events from Webcast, see stacktrace for more info.")) await asyncio.sleep(polling_interval) @@ -190,18 +207,53 @@ async def __fetch_room_data(self, is_initial: bool = False) -> None: """ - webcast_response = await self._http.get_deserialized_object_from_webcast_api("im/fetch/", self._client_params, "WebcastResponse") - _last_cursor, _next_cursor = self._client_params["cursor"], webcast_response.get("cursor") - self._client_params["cursor"] = _last_cursor if _next_cursor == "0" else _next_cursor + # Fetch from polling api + webcast_response = await self._http.get_deserialized_object_from_webcast_api("im/fetch/", self._http.params, "WebcastResponse", is_initial) + _last_cursor, _next_cursor = self._http.params["cursor"], webcast_response.get("cursor") + self._http.params["cursor"] = _last_cursor if _next_cursor == "0" else _next_cursor + # Add param if given if webcast_response.get("internalExt"): - self._client_params["internal_ext"] = webcast_response["internalExt"] + self._http.params["internal_ext"] = webcast_response["internalExt"] - if is_initial and not self._process_initial_data: - return + if is_initial: + if not webcast_response.get("cursor"): + raise InitialCursorMissing("Missing cursor in initial fetch response.") + + # If a WebSocket is offered, upgrade + if bool(webcast_response.get("wsUrl")) and bool(webcast_response.get("wsParam")) and self.__websocket_enabled: + await self.__try_websocket_upgrade(webcast_response) + + # Process initial data if requested + if not self._process_initial_data: + return await self._handle_webcast_messages(webcast_response) + async def __try_websocket_upgrade(self, webcast_response) -> WebcastWebsocket: + """ + Attempt to upgrade the connection to a websocket instead + + :param webcast_response: The initial webcast response including the wsParam and wsUrl items + :return: The websocket, if one is produced + + """ + + socket: WebcastWebsocket = WebcastWebsocket( + client=self, + ws_url=webcast_response.get("wsUrl"), + cookies=self._http.client.cookies, + client_params=self._http.params, + ws_params={"imprp": webcast_response.get("wsParam").get("value")}, + headers=self._http.headers, + loop=self.loop, + ping_interval_ms=self._ping_interval_ms + ) + + self.__is_ws_upgrade_done = await socket.connect() + self._socket = socket if self.__is_ws_upgrade_done else None + return self._socket + async def _handle_webcast_messages(self, webcast_response) -> None: """ Handle the parsing of webcast messages, meant to be overridden by superclass @@ -210,13 +262,14 @@ async def _handle_webcast_messages(self, webcast_response) -> None: raise NotImplementedError - async def _connect(self) -> str: + async def _connect(self, session_id: str = None) -> str: """ - Connect to the Websocket API + Connect to the WebcastWebsocket API :return: The room ID, if connection is successful """ + self.__set_session_id(session_id) if self.__connecting: raise AlreadyConnecting() @@ -235,7 +288,7 @@ async def _connect(self) -> str: # If offline if self.__room_info.get("status", 4) == 4: - raise LiveNotFound() + raise LiveNotFound("The requested user is most likely offline.") # Get extended gift info if self._enable_extended_gift_info: @@ -245,8 +298,19 @@ async def _connect(self) -> str: await self.__fetch_room_data(True) self.__connected = True - # Use request polling (Websockets not implemented) - self.loop.create_task(self.__fetch_room_polling()) + # If the websocket was not connected for whatever reason + if not self.__is_ws_upgrade_done: + # Switch to long polling if a session id was provided + if self._http.client.cookies.get("sessionid"): + self.loop.create_task(self.__fetch_room_polling()) + + else: + # No more options, fail to connect + raise FailedRoomPolling( + ("You have disabled websockets, but not included a sessionid for long polling. " if not self.__websocket_enabled else "") + + "Long polling is not available: Try adding a sessionid as an argument in start() or run()" + ) + return self.__room_id except Exception as ex: @@ -277,11 +341,11 @@ def _disconnect(self) -> None: self.__room_info: Optional[dict] = None self.__connecting: Optional[bool] = False self.__connected: Optional[bool] = False - self._client_params["cursor"]: str = "" + self._http.params["cursor"]: str = "" async def stop(self) -> None: """ - Stop the client + Stop the client safely :return: None @@ -299,8 +363,7 @@ async def start(self, session_id: Optional[str] = None) -> Optional[str]: """ - self.__set_session_id(session_id) - return await self._connect() + return await self._connect(session_id=session_id) def run(self, session_id: Optional[str] = None) -> None: """ @@ -309,9 +372,8 @@ def run(self, session_id: Optional[str] = None) -> None: :return: None """ - self.__set_session_id(session_id) - self.loop.run_until_complete(self._connect()) + self.loop.run_until_complete(self._connect(session_id=session_id)) self.loop.run_forever() def __set_session_id(self, session_id: Optional[str]) -> None: @@ -325,7 +387,7 @@ def __set_session_id(self, session_id: Optional[str]) -> None: if session_id: self.__session_id = session_id - self._http.cookies["sessionid"] = session_id + self._http.client.cookies.set("sessionid", session_id) async def send_message(self, text: str, session_id: Optional[str] = None) -> Optional[str]: """ @@ -333,7 +395,7 @@ async def send_message(self, text: str, session_id: Optional[str] = None) -> Opt :param text: The message you want to send to the chat :param session_id: The Session ID (If you've already supplied one, you don't need to) - :return: None + :return: The response from the webcast API """ @@ -342,8 +404,8 @@ async def send_message(self, text: str, session_id: Optional[str] = None) -> Opt if not self.__session_id: raise InvalidSessionId("Missing Session ID. Please provide your current Session ID to use this feature.") - params: dict = {**self._client_params, "content": text} - response: dict = await self._http.post_json_to_webcast_api("room/chat/", params, None) + params: dict = {**self._http.params, "content": text} + response: dict = await self._http.post_json_to_webcast_api("room/chat/", params, None, sign_url=False) status_code: Optional[int] = response.get("status_code") data: Optional[dict] = response.get("data") @@ -385,114 +447,26 @@ async def retrieve_available_gifts(self) -> Optional[Dict[int, ExtendedGift]]: return await self.__fetch_available_gifts() - async def set_proxies_enabled(self, enabled: bool) -> None: - """ - Set whether to use proxies in requests - - :param enabled: Whether proxies are enabled or not - :return: None - - """ - - self._http.proxy_container.set_enabled(enabled) - - async def add_proxies(self, *proxies: str) -> None: + async def set_proxies(self, proxies: Optional[Dict[str, str]]) -> None: """ - Add proxies to the proxy container for request usage - - :param proxies: Proxies for usage - :return: None - - """ - - for proxy in proxies: - self._http.proxy_container.proxies.append(proxy) - - async def remove_proxies(self, *proxies: str) -> None: - """ - Remove proxies from the proxy container for request usage + Set the proxies to be used by the HTTP client (Not Websockets) - :param proxies: Proxies to remove - :raises ValueError: Raises ValueError if proxy is not present + :param proxies: The proxies to use in HTTP requests :return: None """ - for proxy in proxies: - self._http.proxy_container.proxies.remove(proxy) - - async def get_proxies(self) -> List[str]: - """ - Get a list of the current proxies in the proxy container being used for requests - - :return: The proxies in the request container - """ - - return self._http.proxy_container.proxies - - @property - def viewer_count(self) -> Optional[int]: - """ - Return viewer count of user - - :return: Viewer count - - """ - return self._viewer_count - - @property - def room_id(self) -> Optional[int]: - """ - Room ID if the connection was successful - - :return: Room's ID - - """ - return self.__room_id - - @property - def room_info(self) -> Optional[dict]: - """ - Room info dict if the connection was successful - - :return: Room Info Dict - - """ - - return self.__room_info - - @property - def unique_id(self) -> str: - """ - Unique ID of the streamer - - :return: Their unique ID - - """ - - return self.__unique_id + self._http.proxies = proxies - @property - def connected(self) -> bool: + async def get_proxies(self) -> Optional[Dict[str, str]]: """ - Whether the client is connected + Get the current proxies being used in HTTP requests - :return: Result + :return: The current proxies in use """ - return self.__connected - - @property - def available_gifts(self) -> Dict[int, ExtendedGift]: - """ - Available gift information for live room - - :return: Gift info - - """ - - return self.__available_gifts + return self._http.proxies def download( self, @@ -587,3 +561,67 @@ def stop_download(self) -> None: f"Stopped the download to path \"{self._download.path}\" on user @{self.unique_id} after " f"\"{int(datetime.utcnow().timestamp()) - self._download.started_at} seconds\" of downloading" ) + + @property + def viewer_count(self) -> Optional[int]: + """ + Return viewer count of user + + :return: Viewer count + + """ + return self._viewer_count + + @property + def room_id(self) -> Optional[int]: + """ + Room ID if the connection was successful + + :return: Room's ID + + """ + return self.__room_id + + @property + def room_info(self) -> Optional[dict]: + """ + Room info dict if the connection was successful + + :return: Room Info Dict + + """ + + return self.__room_info + + @property + def unique_id(self) -> str: + """ + Unique ID of the streamer + + :return: Their unique ID + + """ + + return self.__unique_id + + @property + def connected(self) -> bool: + """ + Whether the client is connected + + :return: Result + + """ + + return self.__connected + + @property + def available_gifts(self) -> Dict[int, ExtendedGift]: + """ + Available gift information for live room + + :return: Gift info + + """ + + return self.__available_gifts diff --git a/TikTokLive/client/client.py b/TikTokLive/client/client.py index 1bc811f..3edf5e4 100644 --- a/TikTokLive/client/client.py +++ b/TikTokLive/client/client.py @@ -3,7 +3,6 @@ from typing import Optional, Type, Callable from dacite import from_dict -from pyee import AsyncIOEventEmitter from .base import BaseClient from ..proto.utilities import from_dict_plus @@ -12,7 +11,7 @@ SubscribeEvent, WeeklyRankingEvent, MicBattleEvent, MicArmiesEvent -class TikTokLiveClient(AsyncIOEventEmitter, BaseClient): +class TikTokLiveClient(BaseClient): """ TikTokLive Client responsible for emitting events asynchronously @@ -20,6 +19,7 @@ class TikTokLiveClient(AsyncIOEventEmitter, BaseClient): def __init__(self, unique_id: str, debug: bool = False, **options): """ + Initialize the BaseClient for TikTokLive Webcast tracking :param unique_id: The unique id of the creator to connect to :param debug: Debug mode -> Add all events' raw payload to a "debug" event @@ -28,9 +28,7 @@ def __init__(self, unique_id: str, debug: bool = False, **options): """ self.debug_enabled: bool = debug - BaseClient.__init__(self, unique_id, **options) - AsyncIOEventEmitter.__init__(self, self.loop) async def _on_error(self, original: Exception, append: Optional[Exception]) -> None: """ @@ -60,7 +58,7 @@ async def _on_error(self, original: Exception, append: Optional[Exception]) -> N self._log_error(_exc) return - # If connected, has handler + # If connected, has handler self.emit("error", _exc) @classmethod @@ -79,13 +77,13 @@ def _log_error(cls, exception: Exception) -> None: logging.error(traceback.format_exc()) return - async def _connect(self) -> str: + async def _connect(self, session_id: str = None) -> str: """ Wrap connection in a connect event """ - result: str = await super(TikTokLiveClient, self)._connect() + result: str = await super(TikTokLiveClient, self)._connect(session_id=session_id) if self.connected: event: ConnectEvent = ConnectEvent() @@ -122,6 +120,14 @@ async def _handle_webcast_messages(self, webcast_response: dict) -> None: self.emit("debug", AbstractEvent(data=message)) def __parse_message(self, webcast_message: dict) -> Optional[AbstractEvent]: + """ + Parse a webcast message into an event and return to the caller + + :param webcast_message: The message to parse + :return: The parsed object of base-type AbstractEvent + + """ + event_dict: Optional[dict] = webcast_message.get("event") # It's a traditional event diff --git a/TikTokLive/client/config.py b/TikTokLive/client/config.py new file mode 100644 index 0000000..404836c --- /dev/null +++ b/TikTokLive/client/config.py @@ -0,0 +1,58 @@ +from typing import Dict, Union + +"""Default HTTP client parameters to include in requests to the Webcast API & Websocket Server""" +DEFAULT_CLIENT_PARAMS: Dict[str, Union[int, bool, str]] = { + "aid": 1988, + "app_language": 'en-US', + "app_name": 'tiktok_web', + "browser_language": 'en', + "browser_name": 'Mozilla', + "browser_online": True, + "browser_platform": 'Win32', + "browser_version": '5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36', + "cookie_enabled": True, + "cursor": '', + "internal_ext": '', + "device_platform": 'web', + "focus_state": True, + "from_page": 'user', + "history_len": 4, + "is_fullscreen": False, + "is_page_visible": True, + "did_rule": 3, + "fetch_rule": 1, + "identity": 'audience', + "last_rtt": 0, + "live_id": 12, + "resp_content_type": 'protobuf', + "screen_height": 1152, + "screen_width": 2048, + "tz_name": 'Europe/Berlin', + "referer": 'https://www.tiktok.com/', + "root_referer": 'https://www.tiktok.com/', + "msToken": '', + "version_code": 180800, + "webcast_sdk_version": '1.3.0', + "update_version_code": '1.3.0', +} + +"""Default HTTP client headers to include in requests to the Webcast API & Websocket Server""" +DEFAULT_REQUEST_HEADERS: Dict[str, str] = { + "Connection": 'keep-alive', + 'Cache-Control': 'max-age=0', + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36', + "Accept": 'text/html,application/json,application/protobuf', + "Referer": 'https://www.tiktok.com/', + "Origin": 'https://www.tiktok.com', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate', +} + +"""The URL of the TikTok Webapp""" +TIKTOK_URL_WEB: str = 'https://www.tiktok.com/' + +"""The URL of the Webcast API""" +TIKTOK_URL_WEBCAST: str = 'https://webcast.tiktok.com/webcast/' + +"""The URL of the Webcast External Signing API""" +TIKTOK_SIGN_API: str = "https://tiktok.isaackogan.com/" diff --git a/TikTokLive/client/http.py b/TikTokLive/client/http.py index 8daa167..b56eb04 100644 --- a/TikTokLive/client/http.py +++ b/TikTokLive/client/http.py @@ -1,9 +1,10 @@ +import json as json_parse import urllib.parse -from typing import Dict, Union, Optional +from typing import Dict, Optional -from aiohttp import ClientSession +import httpx -from TikTokLive.client.proxy import ProxyContainer +from TikTokLive.client import config from TikTokLive.proto.utilities import deserialize_message @@ -13,94 +14,114 @@ class TikTokHTTPClient: """ - TIKTOK_URL_WEB: str = 'https://www.tiktok.com/' - TIKTOK_URL_WEBCAST: str = 'https://webcast.tiktok.com/webcast/' - TIKTOK_HTTP_ORIGIN: str = 'https://www.tiktok.com' - - DEFAULT_CLIENT_PARAMS: Dict[str, Union[int, bool, str]] = { - "aid": 1988, "app_name": 'tiktok_web', "browser_name": 'Mozilla', - "browser_online": True, "browser_platform": 'Win32', "version_code": 180800, - "browser_version": '5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36', - "cookie_enabled": True, "cursor": '', "device_platform": 'web', "did_rule": 3, "fetch_rule": 1, "identity": 'audience', "internal_ext": '', - "last_rtt": 0, "live_id": 12, "resp_content_type": 'protobuf', "screen_height": 1152, "screen_width": 2048, "tz_name": 'Europe/Berlin', - "browser_language": "en", "priority_region": "US", "region": "US", - } - - DEFAULT_REQUEST_HEADERS: Dict[str, str] = { - "Connection": 'keep-alive', "Cache-Control": 'max-age=0', "Accept": 'text/html,application/json,application/protobuf', - "User-Agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36', - "Referer": 'https://www.tiktok.com/', "Origin": 'https://www.tiktok.com', "Accept-Language": 'en-US,en;q=0.9', "Accept-Encoding": 'gzip, deflate', - } - - def __init__(self, headers: Optional[Dict[str, str]] = None, timeout_ms: Optional[int] = None, proxy_container: Optional[ProxyContainer] = None, trust_env: bool = True) -> None: + def __init__( + self, + headers: Optional[Dict[str, str]] = None, + timeout_ms: Optional[int] = None, + proxies: Optional[Dict[str, str]] = None, + trust_env: bool = True, + params: Optional[Dict[str, str]] = dict(), + ): """ - Initialize HTTP client + Initialize HTTP client for TikTok-related requests :param headers: Headers to use to make HTTP requests :param timeout_ms: Timeout for HTTP requests - :param proxy_container: Proxy container to hold proxies for request usage + :param proxies: Enable proxied requests by turning on forwarding for the HTTPX "proxies" argument :param trust_env: Whether to trust the environment when it comes to proxy usage """ self.timeout: int = int((timeout_ms if isinstance(timeout_ms, int) else 10000) / 1000) - self.proxy_container: ProxyContainer = proxy_container if proxy_container is not None else ProxyContainer(enabled=False) - self.headers: Dict[str, str] = { - **self.DEFAULT_REQUEST_HEADERS, - **(headers if isinstance(headers, dict) else dict()) - } - self.cookies: dict = dict() + self.proxies: Optional[Dict[str, str]] = proxies + self.headers: Dict[str, str] = {**config.DEFAULT_REQUEST_HEADERS, **(headers if isinstance(headers, dict) else dict())} + self.params: dict = params if params else dict() - async def __aiohttp_get_bytes(self, url: str, params: dict = None) -> bytes: + self.trust_env: bool = trust_env + self.client = httpx.AsyncClient(trust_env=trust_env) + self.__tokens: dict = {} + + @classmethod + def update_url(cls, url: str, params: dict) -> str: """ - Get bytes from a given URL with parameters + Update a URL with given parameters by breaking it into components, adding new ones, and rebuilding it - :param url: URL to request data from - :param params: Custom Parameters - :return: bytearray containing request data - :raises: asyncio.TimeoutError + :param url: The URL we are updating + :param params: The parameters to update it with + :return: The updated URL + + """ + + parsed = list(urllib.parse.urlparse(url)) + query = {**params, **dict(urllib.parse.parse_qsl(parsed[4]))} + parsed[4] = urllib.parse.urlencode(query) + return urllib.parse.urlunparse(parsed) + async def __get_signed_url(self, url: str) -> str: """ - request_url: str = f"{url}?{urllib.parse.urlencode(params if params is not None else dict())}" + Sign a URL via external signing agent to authenticate against TikTok's Webcast API. + This is an API made *for* this library, NOT by TikTok. - async with ClientSession() as session: - async with session.get(request_url, headers=self.headers, timeout=self.timeout, proxy=self.proxy_container.get()) as request: - return await request.read() + :param url: The URL to sign + :return: The signed URL - async def __aiohttp_get_json(self, url: str, params: dict) -> dict: """ - Get json (dict form) from a given URL with parameters + + # Get the signed URL + response: httpx.Response = await self.client.get( + url=f"{config.TIKTOK_SIGN_API}webcast/sign_url?client=ttlive-python&url={urllib.parse.quote(url)}", + timeout=self.timeout + ) + + # Update client information + tokens: dict = response.json() + self.headers["User-Agent"] = tokens.get("User-Agent") + return tokens.get("signedUrl") + + async def __httpx_get_bytes(self, url: str, params: dict = None, sign_url: bool = False) -> bytes: + """ + Get byte data from the Webcast API + + :param url: The URL to request + :param params: Parameters to include in the URL + :param sign_url: Whether to sign the URL (for authenticated endpoints) + :return: The result of the request (a bytearray) + :raises: httpx.TimeoutException + """ + + url: str = self.update_url(url, params if params else dict()) + response: httpx.Response = await self.client.get(await self.__get_signed_url(url) if sign_url else url, headers=self.headers, timeout=self.timeout) + return response.read() + + async def __aiohttp_get_json(self, url: str, params: dict, sign_url: bool = False) -> dict: + """ + Get json (dict) from a given URL with parameters from the Webcast API :param url: URL to request data from :param params: Custom Parameters :return: bytearray containing request data - :raises: asyncio.TimeoutError + :raises: httpx.TimeoutException """ - request_url: str = f"{url}?{urllib.parse.urlencode(params if params is not None else dict())}" + return json_parse.loads((await self.__httpx_get_bytes(url=url, params=params, sign_url=sign_url)).decode(encoding="utf-8")) - async with ClientSession() as session: - async with session.get(request_url, headers=self.headers, timeout=self.timeout, proxy=self.proxy_container.get()) as request: - return await request.json() - - async def __aiohttp_post_json(self, url: str, params: dict, json: Optional[dict] = None) -> dict: + async def __aiohttp_post_json(self, url: str, params: dict, json: Optional[dict] = None, sign_url: bool = False) -> dict: """ Post JSON given a URL with parameters :param url: URL to request data from :param params: Custom Parameters :param json: JSON Payload as Dict + :param sign_url: Whether to sign the URL (for authenticated endpoints) :return: JSON Result - - :raises: asyncio.TimeoutError + :raises: httpx.TimeoutException """ - request_url: str = f"{url}?{urllib.parse.urlencode(params if params is not None else dict())}" - async with ClientSession(cookies=self.cookies) as session: - async with session.post(request_url, data=json, headers=self.headers, timeout=self.timeout, proxy=self.proxy_container.get()) as request: - return await request.json() + url: str = self.update_url(url, params if params else dict()) + response: httpx.Response = await self.client.post(await self.__get_signed_url(url) if sign_url else url, data=json, headers=self.headers, timeout=self.timeout) + return response.json() async def get_livestream_page_html(self, unique_id: str) -> str: """ @@ -108,26 +129,27 @@ async def get_livestream_page_html(self, unique_id: str) -> str: :param unique_id: Unique ID of the streamer :return: HTML string containing page data - :raises: asyncio.TimeoutError + :raises: httpx.TimeoutException """ - response: bytes = await self.__aiohttp_get_bytes(f"{TikTokHTTPClient.TIKTOK_URL_WEB}@{unique_id}/live") + response: bytes = await self.__httpx_get_bytes(f"{config.TIKTOK_URL_WEB}@{unique_id}/live") return response.decode(encoding="utf-8") - async def get_deserialized_object_from_webcast_api(self, path: str, params: dict, schema: str) -> dict: + async def get_deserialized_object_from_webcast_api(self, path: str, params: dict, schema: str, sign_url: bool = False) -> dict: """ Retrieve and deserialize an object from the Webcast API + :param sign_url: Whether to sign the URL (if it's an authenticated request) :param path: Webcast path :param params: Parameters to encode into URL :param schema: Proto schema to decode from :return: Deserialized data from API in dictionary format - :raises: asyncio.TimeoutError + :raises: httpx.TimeoutException """ - response: bytes = await self.__aiohttp_get_bytes(self.TIKTOK_URL_WEBCAST + path, params) + response: bytes = await self.__httpx_get_bytes(config.TIKTOK_URL_WEBCAST + path, params, sign_url=sign_url) return deserialize_message(schema, response) async def get_json_object_from_webcast_api(self, path: str, params: dict) -> dict: @@ -137,23 +159,25 @@ async def get_json_object_from_webcast_api(self, path: str, params: dict) -> dic :param path: Webcast path :param params: Parameters to encode into URL :return: JSON data from Webcast API - :raises: asyncio.TimeoutError + :raises: httpx.TimeoutException """ - response: dict = await self.__aiohttp_get_json(self.TIKTOK_URL_WEBCAST + path, params) + response: dict = await self.__aiohttp_get_json(config.TIKTOK_URL_WEBCAST + path, params) return response.get("data") - async def post_json_to_webcast_api(self, path: str, params: dict, json: Optional[dict] = None): + async def post_json_to_webcast_api(self, path: str, params: dict, json: Optional[dict] = None, sign_url: bool = False) -> dict: """ Post JSON to the Webcast API + :param sign_url: Whether to sign the URL (if it's an authenticated request) :param path: Path to POST :param params: URLEncoded Params :param json: JSON Data - :return: Result + :return: Result from the Webcast API POST request + :raises: httpx.TimeoutException """ - response: dict = await self.__aiohttp_post_json(self.TIKTOK_URL_WEBCAST + path, params, json) + response: dict = await self.__aiohttp_post_json(config.TIKTOK_URL_WEBCAST + path, params, json, sign_url=sign_url) return response diff --git a/TikTokLive/client/proxy.py b/TikTokLive/client/proxy.py deleted file mode 100644 index 574fd2d..0000000 --- a/TikTokLive/client/proxy.py +++ /dev/null @@ -1,113 +0,0 @@ -import enum -import random -from typing import Optional, List - - -class RotationSetting(enum.Enum): - """ - Rotation settings for a proxy container - - """ - - CONSECUTIVE: int = 1 - """Rotate proxies consecutively, from proxy 0 -> 1 -> 2 -> ...etc.""" - - RANDOM: int = 2 - """Rotate proxies randomly, from proxy 0 -> 69 -> 420 -> 1 -> ...etc.""" - - PINNED: int = 3 - """Don't rotate proxies at all, pin to a specific proxy index with set_pinned()""" - - -class ProxyContainer: - - def __init__(self, *proxies: str, mode: int = 1, enabled: bool = True): - """ - Create a ProxyContainer object - - :param proxies: *args containing a list of the proxies - :param mode: The rotation mode as defined in the RotationSetting enum - - """ - - self.proxies: List[str] = list(proxies) - self.__mode: int = mode - self.__index: int = 0 - self.__pin: int = 0 - self.__before_pinned: int = self.__mode - self.__enabled: bool = enabled - - @property - def count(self) -> int: - """ - Get the current number of proxies in the container - - :return: The current number of proxies - - """ - - return len(self.proxies) - - def set_enabled(self, enabled: bool) -> None: - """ - Set whether the system is enabled - - :param enabled: Whether to pull a proxy on get() - :return: None - - """ - - self.__enabled = enabled - - def set_pinned(self, index: int) -> None: - """ - Set the proxy rotator to pinned mode in RotationSetting enum - - :param index: Index to pin to - :return: None - - """ - - self.__pin = index - self.__before_pinned = self.__mode - self.__mode = RotationSetting.PINNED - - def set_unpinned(self) -> None: - """ - Remove pinned status and return to whatever mode was set before set_pinned - - :return:None - - """ - - self.__mode = self.__before_pinned - - def get(self) -> Optional[str]: - """ - Fetch a proxy using one of the rotation settings defined in RotationSetting - - :return: The HTTP/S proxy to return - - """ - - # Has nothing - if self.count < 1 or not self.__enabled: - return None - - # Consecutive - if self.__mode == RotationSetting.CONSECUTIVE: - index: int = self.__index - if index >= self.count: - self.__index, index = 1, 0 - else: - self.__index += 1 - - # Otherwise random - else: - index: int = random.randint(0, self.count - 1) - - # Return a proxy - try: - return self.proxies[index] - except IndexError: - return None diff --git a/TikTokLive/client/websocket.py b/TikTokLive/client/websocket.py new file mode 100644 index 0000000..073b948 --- /dev/null +++ b/TikTokLive/client/websocket.py @@ -0,0 +1,135 @@ +from __future__ import annotations + +import asyncio +import logging +import traceback +from asyncio import AbstractEventLoop +from typing import TYPE_CHECKING, Optional, Dict, Any + +import websockets +from pyee import AsyncIOEventEmitter +from websockets.exceptions import ConnectionClosed +from websockets.legacy.client import WebSocketClientProtocol + +from TikTokLive.client.http import TikTokHTTPClient +from TikTokLive.proto.utilities import deserialize_websocket_message + +if TYPE_CHECKING: + from TikTokLive.client.base import BaseClient + + +class WebcastWebsocket: + """ + Wrapper class to handle websocket connections to the Webcast API + + """ + + def __init__( + self, + client: BaseClient, + ws_url: str, + ws_params: Dict[str, str], + client_params: Dict[str, str], + headers: Dict[str, str], + cookies: Dict[str, str], + ping_interval_ms: float, + loop: AbstractEventLoop, + **kwargs + ): + """ + Initialize Websocket client for Websocket-based Webcast connections + + :param client: The client to emit events back to as they are received + :param ws_url: The URL of the Websocket to connect to + :param ws_params: Parameters to be added to the URL that identify the connection to the websocket + :param client_params: Various regular parameters to be added to the URL + :param headers: Headers to be added to the websocket connection request for authentication + :param cookies: Cookies to be added as a "Cookie" header to the websocket connection request for authentication + :param loop: The main event loop + :param kwargs: Various optional keyword arguments for the websocket itself + :param ping_interval_ms: How often to ping the websocket for room data + + """ + + # Protected Attributes + self._connection: Optional[WebSocketClientProtocol] = None + self._websocket_options: Dict[str, Any] = kwargs + self._ping_interval: float = ping_interval_ms / 1000 + + # Private Attributes + self.__cookies: Dict[str, str] = cookies + self.__ws_params: Dict[str, str] = {**client_params, **ws_params} + self.__headers: Dict[str, str] = {**headers, "Cookie": " ".join(f"{k}={v};" for k, v in cookies.items())} + self.__ws_url: str = TikTokHTTPClient.update_url(ws_url, self.__ws_params) + self.__loop: AbstractEventLoop = loop + self.__client: AsyncIOEventEmitter = client + + async def connect(self) -> bool: + """ + Attempt to connect to the websocket and return the connection status + + :return: Whether the connection was successfully initiated + """ + + try: + # Initiate a connection then keep it open in the connection loop + self._connection = await websockets.connect(uri=self.__ws_url, extra_headers=self.__headers, ssl=True, **self._websocket_options) + self.__loop.create_task(self.connection_loop()) + return True + except: + logging.warning( + f"WebcastWebsocket connection failed, will attempt long polling instead. Consider disabling websockets if this persists: " + f"\n{traceback.format_exc()}" + ) + return False + + async def connection_loop(self) -> None: + """ + The websocket heartbeat, responsible for making requests to the websocket for room data + + :return: None + + """ + + while True: + try: + # Get a response + response: Optional[bytes] = await self._connection.recv() + + # If valid, deserialize response and send back to client + if response: + self.__client.emit("websocket", deserialize_websocket_message(response)) + + except Exception as ex: + # If the connection closed, close the websocket + if isinstance(ex, ConnectionClosed): + await self._connection.close() + self.__client.emit("error", ex) + + # Wait until the next ping time + await asyncio.sleep(self._ping_interval) + + async def is_open(self) -> bool: + """ + Check whether the websocket connection is open + + :return: The result of the check + + """ + + try: + await self._connection.ensure_open() + except: + return False + + return True + + async def close(self) -> None: + """ + Close the websocket connection + + :return: Nothing, you closed it, you dope + + """ + + await self._connection.close() diff --git a/TikTokLive/proto/utilities.py b/TikTokLive/proto/utilities.py index 0fc6352..f35586e 100644 --- a/TikTokLive/proto/utilities.py +++ b/TikTokLive/proto/utilities.py @@ -1,9 +1,8 @@ -from typing import Type, Optional, Any - from dacite import from_dict, Config from dacite.core import T from dacite.data import Data from protobuf_to_dict import protobuf_to_dict +from typing import Type, Optional, Any from TikTokLive.proto import tiktok_schema_pb2 as tiktok_schema @@ -55,6 +54,20 @@ def deserialize_message(proto_name: str, obj: bytes) -> dict: return dict_data +def deserialize_websocket_message(binary_message: bytes) -> dict: + """ + Deserialize Websocket data. Websocket messages are in a container which contains additional data. + A message type 'msg' represents a normal WebcastResponse + + :param binary_message: The binary to decode + :return: The resultant decoded python dictionary + + """ + + decoded: dict = deserialize_message("WebcastWebsocketMessage", binary_message) + return {**decoded, **deserialize_message("WebcastResponse", decoded.get("binary"))} if decoded.get("type") == "msg" else dict() + + def from_dict_plus(data_class: Type[T], data: Data, config: Optional[Config] = None) -> Any: """ Load a schema from a dict and set the _as_dict attribute automatically diff --git a/TikTokLive/types/errors.py b/TikTokLive/types/errors.py index c1ebd1b..effa1ea 100644 --- a/TikTokLive/types/errors.py +++ b/TikTokLive/types/errors.py @@ -30,6 +30,13 @@ class FailedConnection(RuntimeError): pass +class InitialCursorMissing(FailedConnection): + """ + Error that is raised when the initial cursor is missing + + """ + pass + class InvalidSessionId(RuntimeError): """ Error raised when a session ID is expired or missing @@ -56,6 +63,13 @@ class ChatMessageRepeat(ChatMessageSendFailure): pass +class WebsocketConnectionFailed(RuntimeError): + """ + Raised when a connection to the TikTok Webcast websocket fails + + """ + + pass class FailedHTTPRequest(RuntimeError): """ diff --git a/docs/.buildinfo b/docs/.buildinfo index 848d37a..0426d5c 100644 --- a/docs/.buildinfo +++ b/docs/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: 3fb376b4bfbd4d683250a5d5dc0817f5 +config: a9fa3a6db386d2ee39d67223e5e6e9ea tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/README.html b/docs/README.html index 8513101..c409599 100644 --- a/docs/README.html +++ b/docs/README.html @@ -1,118 +1,114 @@ - + - - + - - How To Build — TikTokLive documentation - - - - - - - - + + + + + + - - + + - -
+ +
-
- - -
-
-
- -
-
-
-
- -
-

How To Build

-
    -
  1. Complete commands:

  2. -
-
-
**cd .sphinx
+    
+ +
+
+
+ +
+
+
+
+ +
+

How To Build

+
    +
  1. Complete commands:

  2. +
+
**cd .sphinx
 
 sphinx-apidoc --ext-autodoc --force -o  . ../TikTokLive ../TikTokLive/proto/tiktok_schema_pb2.py
 
 .\make html
-
-
-
-
    -
  1. Move generated sphinx /sphinx/_build/html folder to root, rename to “docs”

  2. -
  3. Add .nojekyll file to new docs folder

  4. -
  5. Add “CNAME” to new docs folder with value tiktoklive.isaackogan.com

  6. -
-
- +
+
+
    +
  1. Move generated sphinx /sphinx/_build/html folder to root, rename to “docs”

  2. +
  3. Add .nojekyll file to new docs folder

  4. +
  5. Add “CNAME” to new docs folder with value tiktoklive.isaackogan.com

  6. +
+
-
-
-
-
+
+
+ -
+
+ - - + + \ No newline at end of file diff --git a/docs/TikTokLive.client.html b/docs/TikTokLive.client.html index b56c1a6..27c6af1 100644 --- a/docs/TikTokLive.client.html +++ b/docs/TikTokLive.client.html @@ -1,918 +1,545 @@ - + - - - - - TikTokLive.client package — TikTokLive documentation - - - - - - - - + + + + + + - - - - + + + + - -
+ +
-
- - -
-
-
- -
-
-
-
- -
-

TikTokLive.client package

-
-

Submodules

-
-
- -

TikTokLive.client.base module

-
-
- class TikTokLive.client.base.BaseClient(unique_id: str, loop: Optional[asyncio.events.AbstractEventLoop] = None, client_params: Optional[dict] = None, headers: Optional[dict] = None, timeout_ms: Optional[int] = None, polling_interval_ms: int = 1000, process_initial_data: bool = True, fetch_room_info_on_connect: bool = True, enable_extended_gift_info: bool = True, trust_env: bool = False, - proxy_container: Optional[TikTokLive.client.proxy.ProxyContainer] = None, lang: Optional[str] = 'en-US')
-

Bases: object

-

Base client responsible for long polling to the TikTok Webcast API

-
-
- async add_proxies(*proxies: str) None -
-

Add proxies to the proxy container for request usage

-
-
Parameters
-

proxies – Proxies for usage

-
-
Returns
-

None

-
-
-
-
- -
-
- property available_gifts: Dict[int, TikTokLive.types.objects.ExtendedGift]
-

Available gift information for live room

-
-
Returns
-

Gift info

-
-
-
-
- -
-
- property connected: bool -
-

Whether the client is connected

-
-
Returns
-

Result

-
-
-
-
- -
-
- download(path: str, duration: Optional[int] = None, verbose: bool = True, loglevel: str = 'error', global_options: Set[str] = {}, inputs: Dict[str, str] = {}, outputs: Dict[str, str] = {}) - None -
-

Start downloading the user’s livestream video for a given duration, NON-BLOCKING via Python Threading

-
-
Parameters
-
-
    -
  • loglevel – Set the FFmpeg log level

  • -
  • outputs – Pass custom params to FFmpeg outputs

  • -
  • inputs – Pass custom params to FFmpeg inputs

  • -
  • global_options – Pass custom params to FFmpeg global options

  • -
  • path – The path to download the livestream video to

  • -
  • duration – If duration is None or less than 1, download will go forever

  • -
  • verbose – Whether to log info about the download in console

  • -
-
-
Returns
-

None

-
-
Raises
-

AlreadyDownloadingStream if already downloading and attempting to start a second download

-
-
-
-
- -
-
- async get_proxies() List[str]
-

Get a list of the current proxies in the proxy container being used for requests

-
-
Returns
-

The proxies in the request container

-
-
-
-
- -
-
- async remove_proxies(*proxies: str) None -
-

Remove proxies from the proxy container for request usage

-
-
Parameters
-

proxies – Proxies to remove

-
-
Raises
-

ValueError – Raises ValueError if proxy is not present

-
-
Returns
-

None

-
-
-
-
- -
-
- async retrieve_available_gifts() Optional[Dict[int, TikTokLive.types.objects.ExtendedGift]]
-

Retrieve available gifts from Webcast API

-
-
Returns
-

None

-
-
-
-
- -
-
- async retrieve_room_info() Optional[dict] -
-

Method to retrieve room information

-
-
Returns
-

Dictionary containing all room info

-
-
-
-
- -
-
- property room_id: Optional[int]
-

Room ID if the connection was successful

-
-
Returns
-

Room’s ID

-
-
-
-
- -
-
- property room_info: Optional[dict]
-

Room info dict if the connection was successful

-
-
Returns
-

Room Info Dict

-
-
-
-
- -
-
- run(session_id: Optional[str] = None) None -
-

Run client while blocking main thread

-
-
Returns
-

None

-
-
-
-
- -
-
- async send_message(text: str, session_id: Optional[str] = None) Optional[str]
-

Send a message to the TikTok Live Chat

-
-
Parameters
-
-
    -
  • text – The message you want to send to the chat

  • -
  • session_id – The Session ID (If you’ve already supplied one, you don’t need to)

  • -
-
-
Returns
-

None

-
-
-
-
- -
-
- async set_proxies_enabled(enabled: bool) None
-

Set whether to use proxies in requests

-
-
Parameters
-

enabled – Whether proxies are enabled or not

-
-
Returns
-

None

-
-
-
-
- -
-
- async start(session_id: Optional[str] = None) Optional[str]
-

Start the client without blocking the main thread

-
-
Returns
-

Room ID that was connected to

-
-
-
-
- -
-
- async stop() None
-

Stop the client

-
-
Returns
-

None

-
-
-
-
- -
-
- stop_download() None
-

Stop downloading a livestream if currently downloading

-
-
Returns
-

None

-
-
Raises
-
- -
-
-
-
- -
-
- property unique_id: str -
-

Unique ID of the streamer

-
-
Returns
-

Their unique ID

-
-
-
-
- -
-
- property viewer_count: Optional[int]
-

Return viewer count of user

-
-
Returns
-

Viewer count

-
-
-
-
- -
-
- -
-
- -

TikTokLive.client.client module

-
-
- class TikTokLive.client.client.TikTokLiveClient(unique_id: str, debug: bool = False, **options)
-

Bases: pyee.AsyncIOEventEmitter, TikTokLive.client.base.BaseClient

-

TikTokLive Client responsible for emitting events asynchronously

-
-
- -
-
- -

TikTokLive.client.http module

-
-
- class TikTokLive.client.http.TikTokHTTPClient(headers: Optional[Dict[str, str]] = None, timeout_ms: Optional[int] = None, proxy_container: Optional[TikTokLive.client.proxy.ProxyContainer] = None, trust_env: bool = True)
-

Bases: object

-

Client for making HTTP requests to TikTok’s Webcast API

-
-
- DEFAULT_CLIENT_PARAMS: Dict[str, Union[int, bool, str]] = {'aid': 1988, - 'app_name': 'tiktok_web', 'browser_language': 'en', 'browser_name': - 'Mozilla', 'browser_online': True, 'browser_platform': 'Win32', 'browser_version': '5.0 (Windows NT - 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, - like Gecko) Chrome/102.0.5005.63 Safari/537.36', 'cookie_enabled': - True, 'cursor': '', 'device_platform': 'web', - 'did_rule': 3, 'fetch_rule': 1, 'identity': - 'audience', 'internal_ext': '', 'last_rtt': 0, - 'live_id': 12, 'priority_region': 'US', 'region': 'US', 'resp_content_type': 'protobuf', 'screen_height': - 1152, 'screen_width': 2048, 'tz_name': 'Europe/Berlin', - 'version_code': 180800}
-
-
- -
-
- DEFAULT_REQUEST_HEADERS: Dict[str, str] = {'Accept': 'text/html,application/json,application/protobuf', - 'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'en-US,en;q=0.9', 'Cache-Control': 'max-age=0', 'Connection': 'keep-alive', 'Origin': 'https://www.tiktok.com', 'Referer': 'https://www.tiktok.com/', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36'}
-
-
- -
-
- TIKTOK_HTTP_ORIGIN: str = 'https://www.tiktok.com'
-
-
- -
-
- TIKTOK_URL_WEB: str = 'https://www.tiktok.com/'
-
-
- -
-
- TIKTOK_URL_WEBCAST: str = 'https://webcast.tiktok.com/webcast/'
-
-
- -
-
- async get_deserialized_object_from_webcast_api(path: str, params: dict, schema: str) dict
-

Retrieve and deserialize an object from the Webcast API

-
-
Parameters
-
-
    -
  • path – Webcast path

  • -
  • params – Parameters to encode into URL

  • -
  • schema – Proto schema to decode from

  • -
-
-
Returns
-

Deserialized data from API in dictionary format

-
-
Raises
-

asyncio.TimeoutError

-
-
-
-
- -
-
- async get_json_object_from_webcast_api(path: str, params: dict) dict
-

Retrieve JSON formatted data from the Webcast API

-
-
Parameters
-
-
    -
  • path – Webcast path

  • -
  • params – Parameters to encode into URL

  • -
-
-
Returns
-

JSON data from Webcast API

-
-
Raises
-

asyncio.TimeoutError

-
-
-
-
- -
-
- async get_livestream_page_html(unique_id: str) str -
-

Get livestream page HTML given a unique id

-
-
Parameters
-

unique_id – Unique ID of the streamer

-
-
Returns
-

HTML string containing page data

-
-
Raises
-

asyncio.TimeoutError

-
-
-
-
- -
-
- async post_json_to_webcast_api(path: str, params: dict, json: Optional[dict] = None)
-

Post JSON to the Webcast API

-
-
Parameters
-
-
    -
  • path – Path to POST

  • -
  • params – URLEncoded Params

  • -
  • json – JSON Data

  • -
-
-
Returns
-

Result

-
-
-
-
- -
-
- -
-
- -

TikTokLive.client.proxy module

-
-
- class TikTokLive.client.proxy.ProxyContainer(*proxies: str, mode: int = 1, enabled: bool = True) -
-

Bases: object

-
-
- property count: int -
-

Get the current number of proxies in the container

-
-
Returns
-

The current number of proxies

-
-
-
-
- -
-
- get() Optional[str]
-

Fetch a proxy using one of the rotation settings defined in RotationSetting

-
-
Returns
-

The HTTP/S proxy to return

-
-
-
-
- -
-
- set_enabled(enabled: bool) None
-

Set whether the system is enabled

-
-
Parameters
-

enabled – Whether to pull a proxy on get()

-
-
Returns
-

None

-
-
-
-
- -
-
- set_pinned(index: int) None
-

Set the proxy rotator to pinned mode in RotationSetting enum

-
-
Parameters
-

index – Index to pin to

-
-
Returns
-

None

-
-
-
-
- -
-
- set_unpinned() None
-

Remove pinned status and return to whatever mode was set before set_pinned

-

:return:None

-
-
- -
-
- -
-
- class TikTokLive.client.proxy.RotationSetting(value)
-

Bases: enum.Enum

-

Rotation settings for a proxy container

-
-
- CONSECUTIVE: int = 1 -
-

Rotate proxies consecutively, from proxy 0 -> 1 -> 2 -> …etc.

-
-
- -
-
- PINNED: int = 3
-

Don’t rotate proxies at all, pin to a specific proxy index with set_pinned()

-
-
- -
-
- RANDOM: int = 2
-

Rotate proxies randomly, from proxy 0 -> 69 -> 420 -> 1 -> …etc.

-
-
- -
-
- -
-
- -

Module contents

-
-
- - -
-
- -
+
+ +
+
+
+ +
+
+
+
+ +
+

TikTokLive.client package

+
+

Submodules

+
+
+

TikTokLive.client.base module

+
+
+class TikTokLive.client.base.BaseClient(unique_id: str, loop: Optional[AbstractEventLoop] = None, client_params: Optional[dict] = None, headers: Optional[dict] = None, timeout_ms: Optional[int] = None, ping_interval_ms: int = 1000, process_initial_data: bool = True, enable_extended_gift_info: bool = True, trust_env: bool = False, proxies: Optional[Dict[str, str]] = None, lang: Optional[str] = 'en-US', fetch_room_info_on_connect: bool = True, websocket_enabled: bool = True)
+

Bases: AsyncIOEventEmitter

+

Base client responsible for long polling to the TikTok Webcast API

+
+
+property available_gifts: Dict[int, ExtendedGift]
+

Available gift information for live room

+
+
Returns
+

Gift info

+
+
+
+ +
+
+property connected: bool
+

Whether the client is connected

+
+
Returns
+

Result

+
+
+
+ +
+
+download(path: str, duration: Optional[int] = None, verbose: bool = True, loglevel: str = 'error', global_options: Set[str] = {}, inputs: Dict[str, str] = {}, outputs: Dict[str, str] = {}) None
+

Start downloading the user’s livestream video for a given duration, NON-BLOCKING via Python Threading

+
+
Parameters
+
    +
  • loglevel – Set the FFmpeg log level

  • +
  • outputs – Pass custom params to FFmpeg outputs

  • +
  • inputs – Pass custom params to FFmpeg inputs

  • +
  • global_options – Pass custom params to FFmpeg global options

  • +
  • path – The path to download the livestream video to

  • +
  • duration – If duration is None or less than 1, download will go forever

  • +
  • verbose – Whether to log info about the download in console

  • +
+
+
Returns
+

None

+
+
Raises
+

AlreadyDownloadingStream if already downloading and attempting to start a second download

+
+
+
+ +
+
+async get_proxies() Optional[Dict[str, str]]
+

Get the current proxies being used in HTTP requests

+
+
Returns
+

The current proxies in use

+
+
+
+ +
+
+async retrieve_available_gifts() Optional[Dict[int, ExtendedGift]]
+

Retrieve available gifts from Webcast API

+
+
Returns
+

None

+
+
+
+ +
+
+async retrieve_room_info() Optional[dict]
+

Method to retrieve room information

+
+
Returns
+

Dictionary containing all room info

+
+
+
+ +
+
+property room_id: Optional[int]
+

Room ID if the connection was successful

+
+
Returns
+

Room’s ID

+
+
+
+ +
+
+property room_info: Optional[dict]
+

Room info dict if the connection was successful

+
+
Returns
+

Room Info Dict

+
+
+
+ +
+
+run(session_id: Optional[str] = None) None
+

Run client while blocking main thread

+
+
Returns
+

None

+
+
+
+ +
+
+async send_message(text: str, session_id: Optional[str] = None) Optional[str]
+

Send a message to the TikTok Live Chat

+
+
Parameters
+
    +
  • text – The message you want to send to the chat

  • +
  • session_id – The Session ID (If you’ve already supplied one, you don’t need to)

  • +
+
+
Returns
+

The response from the webcast API

+
+
+
+ +
+
+async set_proxies(proxies: Optional[Dict[str, str]]) None
+

Set the proxies to be used by the HTTP client (Not Websockets)

+
+
Parameters
+

proxies – The proxies to use in HTTP requests

+
+
Returns
+

None

+
+
+
+ +
+
+async start(session_id: Optional[str] = None) Optional[str]
+

Start the client without blocking the main thread

+
+
Returns
+

Room ID that was connected to

+
+
+
+ +
+
+async stop() None
+

Stop the client safely

+
+
Returns
+

None

+
+
+
+ +
+
+stop_download() None
+

Stop downloading a livestream if currently downloading

+
+
Returns
+

None

+
+
Raises
+
+
+
+
+ +
+
+property unique_id: str
+

Unique ID of the streamer

+
+
Returns
+

Their unique ID

+
+
+
+ +
+
+property viewer_count: Optional[int]
+

Return viewer count of user

+
+
Returns
+

Viewer count

+
+
+
+ +
+ +
+
+

TikTokLive.client.client module

+
+
+class TikTokLive.client.client.TikTokLiveClient(unique_id: str, debug: bool = False, **options)
+

Bases: BaseClient

+

TikTokLive Client responsible for emitting events asynchronously

+
+ +
+
+

TikTokLive.client.config module

+
+
+TikTokLive.client.config.DEFAULT_CLIENT_PARAMS: Dict[str, Union[int, bool, str]] = {'aid': 1988, 'app_language': 'en-US', 'app_name': 'tiktok_web', 'browser_language': 'en', 'browser_name': 'Mozilla', 'browser_online': True, 'browser_platform': 'Win32', 'browser_version': '5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36', 'cookie_enabled': True, 'cursor': '', 'device_platform': 'web', 'did_rule': 3, 'fetch_rule': 1, 'focus_state': True, 'from_page': 'user', 'history_len': 4, 'identity': 'audience', 'internal_ext': '', 'is_fullscreen': False, 'is_page_visible': True, 'last_rtt': 0, 'live_id': 12, 'msToken': '', 'referer': 'https://www.tiktok.com/', 'resp_content_type': 'protobuf', 'root_referer': 'https://www.tiktok.com/', 'screen_height': 1152, 'screen_width': 2048, 'tz_name': 'Europe/Berlin', 'update_version_code': '1.3.0', 'version_code': 180800, 'webcast_sdk_version': '1.3.0'}
+

Default HTTP client headers to include in requests to the Webcast API & Websocket Server

+
+ +
+
+TikTokLive.client.config.DEFAULT_REQUEST_HEADERS: Dict[str, str] = {'Accept': 'text/html,application/json,application/protobuf', 'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'en-US,en;q=0.9', 'Cache-Control': 'max-age=0', 'Connection': 'keep-alive', 'Origin': 'https://www.tiktok.com', 'Referer': 'https://www.tiktok.com/', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36'}
+

The URL of the TikTok Webapp

+
+ +
+
+TikTokLive.client.config.TIKTOK_URL_WEB: str = 'https://www.tiktok.com/'
+

The URL of the Webcast API

+
+ +
+
+TikTokLive.client.config.TIKTOK_URL_WEBCAST: str = 'https://webcast.tiktok.com/webcast/'
+

The URL of the Webcast External Signing API

+
+ +
+
+

TikTokLive.client.http module

+
+
+class TikTokLive.client.http.TikTokHTTPClient(headers: Optional[Dict[str, str]] = None, timeout_ms: Optional[int] = None, proxies: Optional[Dict[str, str]] = None, trust_env: bool = True, params: Optional[Dict[str, str]] = {})
+

Bases: object

+

Client for making HTTP requests to TikTok’s Webcast API

+
+
+async get_deserialized_object_from_webcast_api(path: str, params: dict, schema: str, sign_url: bool = False) dict
+

Retrieve and deserialize an object from the Webcast API

+
+
Parameters
+
    +
  • sign_url – Whether to sign the URL (if it’s an authenticated request)

  • +
  • path – Webcast path

  • +
  • params – Parameters to encode into URL

  • +
  • schema – Proto schema to decode from

  • +
+
+
Returns
+

Deserialized data from API in dictionary format

+
+
Raises
+

httpx.TimeoutException

+
+
+
+ +
+
+async get_json_object_from_webcast_api(path: str, params: dict) dict
+

Retrieve JSON formatted data from the Webcast API

+
+
Parameters
+
    +
  • path – Webcast path

  • +
  • params – Parameters to encode into URL

  • +
+
+
Returns
+

JSON data from Webcast API

+
+
Raises
+

httpx.TimeoutException

+
+
+
+ +
+
+async get_livestream_page_html(unique_id: str) str
+

Get livestream page HTML given a unique id

+
+
Parameters
+

unique_id – Unique ID of the streamer

+
+
Returns
+

HTML string containing page data

+
+
Raises
+

httpx.TimeoutException

+
+
+
+ +
+
+async post_json_to_webcast_api(path: str, params: dict, json: Optional[dict] = None, sign_url: bool = False) dict
+

Post JSON to the Webcast API

+
+
Parameters
+
    +
  • sign_url – Whether to sign the URL (if it’s an authenticated request)

  • +
  • path – Path to POST

  • +
  • params – URLEncoded Params

  • +
  • json – JSON Data

  • +
+
+
Returns
+

Result from the Webcast API POST request

+
+
Raises
+

httpx.TimeoutException

+
+
+
+ +
+
+classmethod update_url(url: str, params: dict) str
+

Update a URL with given parameters by breaking it into components, adding new ones, and rebuilding it

+
+
Parameters
+
    +
  • url – The URL we are updating

  • +
  • params – The parameters to update it with

  • +
+
+
Returns
+

The updated URL

+
+
+
+ +
+ +
+
+

TikTokLive.client.websocket module

+
+
+class TikTokLive.client.websocket.WebcastWebsocket(client: BaseClient, ws_url: str, ws_params: Dict[str, str], client_params: Dict[str, str], headers: Dict[str, str], cookies: Dict[str, str], ping_interval_ms: float, loop: AbstractEventLoop, **kwargs)
+

Bases: object

+

Wrapper class to handle websocket connections to the Webcast API

+
+
+async close() None
+

Close the websocket connection

+
+
Returns
+

Nothing, you closed it, you dope

+
+
+
+ +
+
+async connect() bool
+

Attempt to connect to the websocket and return the connection status

+
+
Returns
+

Whether the connection was successfully initiated

+
+
+
+ +
+
+async connection_loop() None
+

The websocket heartbeat, responsible for making requests to the websocket for room data

+
+
Returns
+

None

+
+
+
+ +
+
+async is_open() bool
+

Check whether the websocket connection is open

+
+
Returns
+

The result of the check

+
+
+
+ +
+ +
+
+

Module contents

+
+
+ + +
+
+
+
-
- +
+ \ No newline at end of file diff --git a/docs/TikTokLive.html b/docs/TikTokLive.html index c72ce22..ee4a6dd 100644 --- a/docs/TikTokLive.html +++ b/docs/TikTokLive.html @@ -1,201 +1,179 @@ - + - - + - - TikTokLive package — TikTokLive documentation - - - - - - - - + + + + + + - - - - + + + + - -
+ +
-
- - -
-
-
- -
-
-
-
- -
-

TikTokLive package

-
-

Subpackages

- -
-
-

Submodules

-
-
- -

TikTokLive.utils module

-
-
- TikTokLive.utils.get_room_id_from_main_page_html(main_page_html: str) str
-

Get the room ID from the HTML of the creator’s page. - If this fails, you are probably blocked from TikTok. Use a VPN.

-
-
Returns
-

The client’s Room ID

-
-
-
-
- -
-
- TikTokLive.utils.validate_and_normalize_unique_id(unique_id: str) str -
-

Take a host of unique_id formats and convert them into a “normalized” version. - For example, “@tiktoklive” -> “tiktoklive”

-
-
Returns
-

Normalized version of the unique_id

-
-
-
-
- -
-
- -

Module contents

-
-
- - -
-
- -
+
+ +
+
+
+ +
+
+
+
+ +
+

TikTokLive package

+
+

Subpackages

+ +
+
+

Submodules

+
+
+

TikTokLive.utils module

+
+
+TikTokLive.utils.get_room_id_from_main_page_html(main_page_html: str) str
+

Get the room ID from the HTML of the creator’s page. +If this fails, you are probably blocked from TikTok. Use a VPN.

+
+
Returns
+

The client’s Room ID

+
+
+
+ +
+
+TikTokLive.utils.validate_and_normalize_unique_id(unique_id: str) str
+

Take a host of unique_id formats and convert them into a “normalized” version. +For example, “@tiktoklive” -> “tiktoklive”

+
+
Returns
+

Normalized version of the unique_id

+
+
+
+ +
+
+

Module contents

+
+
+ + +
+
+
+
-
- +
+ \ No newline at end of file diff --git a/docs/TikTokLive.proto.html b/docs/TikTokLive.proto.html index 3078c25..6b7458f 100644 --- a/docs/TikTokLive.proto.html +++ b/docs/TikTokLive.proto.html @@ -1,194 +1,180 @@ - + - - + - - TikTokLive.proto package — TikTokLive documentation - - - - - - - - + + + + + + - - - - + + + + - -
+ +
-
- - -
-
-
- -
-
-
-
- -
-

TikTokLive.proto package

-
-

Submodules

-
-
- -

TikTokLive.proto.utilities module

-
-
- TikTokLive.proto.utilities.deserialize_message(proto_name: str, obj: bytes) dict
-

Deserialize a protobuf message into a dictionary

-
-
Parameters
-
-
    -
  • proto_name – The name of the message

  • -
  • obj – The protobuf object to deserialize

  • -
-
-
Returns
-

The dictionary containing the deserialized message

-
-
-
-
- -
-
- TikTokLive.proto.utilities.from_dict_plus(data_class: Type[dacite.core.T], data: Dict[str, Any], config: Optional[dacite.config.Config] = None) Any
-

Load a schema from a dict and set the _as_dict attribute automatically

-
-
Parameters
-
-
    -
  • data_class – Data class schema

  • -
  • data – Data to fit into data class

  • -
  • config – Config for dacite

  • -
-
-
Returns
-

A dataclass containing type T

-
-
-
-
- -
-
- -

Module contents

-
-
- - -
-
- -
+
+ +
+
+
+ +
+
+
+
+ +
+

TikTokLive.proto package

+
+

Submodules

+
+
+

TikTokLive.proto.utilities module

+
+
+TikTokLive.proto.utilities.deserialize_message(proto_name: str, obj: bytes) dict
+

Deserialize a protobuf message into a dictionary

+
+
Parameters
+
    +
  • proto_name – The name of the message

  • +
  • obj – The protobuf object to deserialize

  • +
+
+
Returns
+

The dictionary containing the deserialized message

+
+
+
+ +
+
+TikTokLive.proto.utilities.deserialize_websocket_message(binary_message: bytes) dict
+

Deserialize Websocket data. Websocket messages are in a container which contains additional data. +A message type ‘msg’ represents a normal WebcastResponse

+
+
Parameters
+

binary_message – The binary to decode

+
+
Returns
+

The resultant decoded python dictionary

+
+
+
+ +
+
+TikTokLive.proto.utilities.from_dict_plus(data_class: Type[T], data: Dict[str, Any], config: Optional[Config] = None) Any
+

Load a schema from a dict and set the _as_dict attribute automatically

+
+
Parameters
+
    +
  • data_class – Data class schema

  • +
  • data – Data to fit into data class

  • +
  • config – Config for dacite

  • +
+
+
Returns
+

A dataclass containing type T

+
+
+
+ +
+
+

Module contents

+
+
+ + +
+
+
+
-
- +
+ \ No newline at end of file diff --git a/docs/TikTokLive.types.html b/docs/TikTokLive.types.html index ad2d7f7..e7469c2 100644 --- a/docs/TikTokLive.types.html +++ b/docs/TikTokLive.types.html @@ -1,2589 +1,1481 @@ - + - - - - - TikTokLive.types package — TikTokLive documentation - - - - - - - - + + + + + + - - - + + + - -
+ +
-
- - -
-
-
- -
-
-
-
- -
-

TikTokLive.types package

-
-

Submodules

-
-
- -

TikTokLive.types.errors module

-
-
- exception TikTokLive.types.errors.AlreadyConnected
-

Bases: RuntimeError

-

Error that is raised when attempting to connect to a livestream whilst already connected.

-
-
- -
-
- exception TikTokLive.types.errors.AlreadyConnecting
-

Bases: RuntimeError

-

Error that is raised when attempting to connect to a livestream whilst already attempting to connect.

-
-
- -
-
- exception TikTokLive.types.errors.AlreadyDownloadingStream
-

Bases: TikTokLive.types.errors.DownloadStreamError

-

Error raised when already downloading a livestream and one attempts to start a second download

-
-
- -
-
- exception TikTokLive.types.errors.ChatMessageRepeat
-

Bases: TikTokLive.types.errors.ChatMessageSendFailure

-

Error raised when someone repeats a chat message

-
-
- -
-
- exception TikTokLive.types.errors.ChatMessageSendFailure
-

Bases: RuntimeError

-

Error raised when a TikTok chat message fails to send

-
-
- -
-
- exception TikTokLive.types.errors.DownloadProcessNotFound
-

Bases: TikTokLive.types.errors.DownloadStreamError

-

Error raised when stopping a download and the process is not found. Usually, you’re stopping it before the process spawns

-
-
- -
-
- exception TikTokLive.types.errors.DownloadStreamError
-

Bases: RuntimeError

-

Error raised for anything relating to downloading streams

-
-
- -
-
- exception TikTokLive.types.errors.FailedConnection
-

Bases: RuntimeError

-

Error that is raised when the connection to a livestream fails (generic).

-
-
- -
-
- exception TikTokLive.types.errors.FailedFetchGifts
-

Bases: TikTokLive.types.errors.FailedHTTPRequest

-

Error raised when fetching gifts encounters an exception

-
-
- -
-
- exception TikTokLive.types.errors.FailedFetchRoomInfo
-

Bases: TikTokLive.types.errors.FailedHTTPRequest

-

Error raised when failing to fetch room info

-
-
- -
-
- exception TikTokLive.types.errors.FailedHTTPRequest
-

Bases: RuntimeError

-

Error raised whenever a request fails to HTTP [Generic]

-
-
- -
-
- exception TikTokLive.types.errors.FailedRoomPolling
-

Bases: TikTokLive.types.errors.FailedHTTPRequest

-

Error raised when room polling encounters an exception

-
-
- -
-
- exception TikTokLive.types.errors.InvalidSessionId
-

Bases: RuntimeError

-

Error raised when a session ID is expired or missing

-
-
- -
-
- exception TikTokLive.types.errors.LiveNotFound
-

Bases: RuntimeError

-

Error that is raised when the livestream you are trying to connect to is offline/does-not-exist.

-
-
- -
-
- exception TikTokLive.types.errors.NotDownloadingStream
-

Bases: TikTokLive.types.errors.DownloadStreamError

-

Error raised when trying to stop downloading a livestream you are not currently downloading

-
-
- -
-
- -

TikTokLive.types.events module

-
-
- class TikTokLive.types.events.AbstractEvent(data: dict = {}) -
-

Bases: object

-

Abstract Event

-
-
- property as_dict: dict -
-

Return a copy of the object as a dictionary

-
-
Returns
-

A copy of the raw payload

-
-
-
-
- -
-
- name: str = 'event'
-
-
- -
-
- set_as_dict(data: dict)
-

Set that _as_dict attribute

-
-
Parameters
-

data – The data to set it to

-
-
Returns
-

None

-
-
-
-
- -
-
- -
-
- class TikTokLive.types.events.CommentEvent(user: Optional[TikTokLive.types.objects.User], comment: Optional[str], name: str = 'comment')
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fires when someone comments on the livestream

-
-
- comment: Optional[str]
-

The UTF-8 text comment that was sent

-
-
- -
-
- name: str = 'comment'
-
-
- -
-
- user: Optional[TikTokLive.types.objects.User]
-

The user that sent the comment

-
-
- -
-
- -
-
- class TikTokLive.types.events.ConnectEvent(name: str = 'connect')
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fires when the client connect to a livestream

-
-
- name: str = 'connect'
-
-
- -
-
- -
-
- class TikTokLive.types.events.DisconnectEvent(name: str = 'disconnect')
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fires when the client disconnects from a livestream

-
-
- name: str = 'disconnect'
-
-
- -
-
- -
-
- class TikTokLive.types.events.EmoteEvent(user: Optional[TikTokLive.types.objects.User], emote: Optional[TikTokLive.types.objects.Emote], name: str = 'emote') -
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fires when someone sends a subscriber emote

-
-
- emote: Optional[TikTokLive.types.objects.Emote]
-

The emote the person sent

-
-
- -
-
- name: str = 'emote'
-
-
- -
-
- user: Optional[TikTokLive.types.objects.User]
-

Person who sent the emote message

-
-
- -
-
- -
-
- class TikTokLive.types.events.EnvelopeEvent(treasureBoxData: Optional[TikTokLive.types.objects.TreasureBoxData], treasureBoxUser: Optional[TikTokLive.types.objects.User], name: str = 'envelope')
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fire when someone sends an envelope

-
-
- name: str = 'envelope'
-
-
- -
-
- treasureBoxData: Optional[TikTokLive.types.objects.TreasureBoxData]
-

Data about the enclosed Treasure Box in the envelope

-
-
- -
-
- treasureBoxUser: Optional[TikTokLive.types.objects.User]
-

Data about the user that sent the treasure box

-
-
- -
-
- -
-
- class TikTokLive.types.events.FollowEvent(user: Optional[TikTokLive.types.objects.User], displayType: Optional[str], label: Optional[str], name: str = 'follow')
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fires when a user follows the livestream

-
-
- displayType: Optional[str]
-
-
- -
-
- label: Optional[str]
-
-
- -
-
- name: str = 'follow'
-
-
- -
-
- user: Optional[TikTokLive.types.objects.User]
-

The user that followed the streamer

-
-
- -
-
- -
-
- class TikTokLive.types.events.GiftEvent(user: Optional[TikTokLive.types.objects.User], gift: Optional[TikTokLive.types.objects.Gift], name: str = 'gift') -
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fires when a gift is received

-
-
- gift: Optional[TikTokLive.types.objects.Gift]
-

Object containing gift data

-
-
- -
-
- name: str = 'gift'
-
-
- -
-
- user: Optional[TikTokLive.types.objects.User]
-

The user that sent the gift

-
-
- -
-
- -
-
- class TikTokLive.types.events.JoinEvent(user: Optional[TikTokLive.types.objects.User], displayType: Optional[str], label: Optional[str], name: str = 'join')
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fires when a user joins the livestream

-
-
- displayType: Optional[str]
-

The type of event

-
-
- -
-
- label: Optional[str]
-

Label for event in live chat

-
-
- -
-
- name: str = 'join'
-
-
- -
-
- property through_share: bool -
-

Whether they joined through a link vs. the TikTok App

-
-
Returns
-

Returns True if they joined through a share link

-
-
-
-
- -
-
- user: Optional[TikTokLive.types.objects.User]
-

The user that joined the stream

-
-
- -
-
- -
-
- class TikTokLive.types.events.LikeEvent(user: Optional[TikTokLive.types.objects.User], likeCount: Optional[int], totalLikeCount: Optional[int], displayType: Optional[str], label: Optional[str], name: str = 'like')
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fires when a user likes the livestream

-
-
- displayType: Optional[str]
-
-
- -
-
- label: Optional[str]
-
-
- -
-
- likeCount: Optional[int]
-

The number of likes they sent (I think?)

-
-
- -
-
- name: str = 'like'
-
-
- -
-
- totalLikeCount: Optional[int]
-

The total number of likes on the stream

-
-
- -
-
- user: Optional[TikTokLive.types.objects.User]
-

The user that liked the stream

-
-
- -
-
- -
-
- class TikTokLive.types.events.LiveEndEvent(name: str = 'live_end')
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fires when the livestream ends

-
-
- name: str = 'live_end'
-
-
- -
-
- -
-
- class TikTokLive.types.events.MicArmiesEvent(battleStatus: typing.Optional[int], battleUsers: typing.List[TikTokLive.types.objects.MicArmiesUser] - = <factory>, name: str = 'mic_armies')
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fires during a Mic Battle to update its progress

-
-
- battleStatus: Optional[int]
-

The status of the current Battle

-
-
- -
-
- battleUsers: List[TikTokLive.types.objects.MicArmiesUser]
-

Information about the users engaged in the Mic Battle

-
-
- -
-
- name: str = 'mic_armies'
-
-
- -
-
- -
-
- class TikTokLive.types.events.MicBattleEvent(battleUsers: typing.List[TikTokLive.types.objects.MicBattleUser] = <factory>, name: str = 'mic_battle')
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fires when a Mic Battle starts

-
-
- battleUsers: List[TikTokLive.types.objects.MicBattleUser]
-

Information about the users engaged in the Mic Battle

-
-
- -
-
- name: str = 'mic_battle'
-
-
- -
-
- -
-
- class TikTokLive.types.events.MoreShareEvent(user: Optional[TikTokLive.types.objects.User], displayType: Optional[str], label: Optional[str], name: str = 'more_share') -
-

Bases: TikTokLive.types.events.ShareEvent

-

Event that fires when a user shared the livestream more than 5 users or more than 10 users

-

“user123 has shared to more than 10 people!”

-
-
- property amount: Optional[int]
-

The number of people that have joined the stream off the user

-
-
Returns
-

The number of people that have joined

-
-
-
-
- -
-
- name: str = 'more_share'
-
-
- -
-
- -
-
- class TikTokLive.types.events.QuestionEvent(questionText: Optional[str], user: Optional[TikTokLive.types.objects.User], name: str = 'question')
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fires when someone asks a Q&A question

-
-
- name: str = 'question'
-
-
- -
-
- questionText: Optional[str]
-

The question that was asked

-
-
- -
-
- user: Optional[TikTokLive.types.objects.User]
-

User who asked the question

-
-
- -
-
- -
-
- class TikTokLive.types.events.ShareEvent(user: Optional[TikTokLive.types.objects.User], displayType: Optional[str], label: Optional[str], name: str = 'share')
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fires when a user shares the livestream

-
-
- displayType: Optional[str]
-

Type of event

-
-
- -
-
- label: Optional[str]
-

Internal Webcast Label

-
-
- -
-
- name: str = 'share'
-
-
- -
-
- user: Optional[TikTokLive.types.objects.User]
-

The user that shared the stream

-
-
- -
-
- -
-
- class TikTokLive.types.events.SubscribeEvent(user: Optional[TikTokLive.types.objects.User], actionId: Optional[int], event: Optional[TikTokLive.types.objects.MemberMessage], name: str = 'subscribe')
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fires when someone subscribes to the streamer

-
-
- actionId: Optional[int]
-

The actionId of the MemberMessage corresponding to a sub (actionId=7)

-
-
- -
-
- event: Optional[TikTokLive.types.objects.MemberMessage] -
-

The details of the MemberMessage resulting in a subscription

-
-
- -
-
- name: str = 'subscribe'
-
-
- -
-
- user: Optional[TikTokLive.types.objects.User]
-

The user that subscribed to the streamer

-
-
- -
-
- -
-
- class TikTokLive.types.events.UnknownEvent(name: str = 'unknown')
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fires when an event is received that is not handled by other events in the library.

-
-
- name: str = 'unknown'
-
-
- -
-
- -
-
- class TikTokLive.types.events.ViewerCountUpdateEvent(viewerCount: Optional[int], name: str = 'viewer_count_update')
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fires when the viewer count for the livestream updates

-
-
- name: str = 'viewer_count_update'
-
-
- -
-
- viewerCount: Optional[int]
-

The number of people viewing the stream currently

-
-
- -
-
- -
-
- class TikTokLive.types.events.WeeklyRankingEvent(data: Optional[TikTokLive.types.objects.RankContainer], name: str = 'weekly_ranking')
-

Bases: TikTokLive.types.events.AbstractEvent

-

Event that fires when the weekly rankings are updated

-
-
- data: Optional[TikTokLive.types.objects.RankContainer] -
-

Weekly ranking data

-
-
- -
-
- name: str = 'weekly_ranking'
-
-
- -
-
- -
-
- -

TikTokLive.types.objects module

-
-
- class TikTokLive.types.objects.AbstractObject -
-

Bases: object

-

Abstract Object

-
-
- -
-
- class TikTokLive.types.objects.Avatar(urls: List[str])
-

Bases: TikTokLive.types.objects.AbstractObject

-

The URLs to the avatar of a TikTok User

-
-
- property avatar_url
-

The last (highest quality) avatar URL supplied

-
-
- -
-
- urls: List[str]
-
-
- -
-
- -
-
- class TikTokLive.types.objects.Badge(type: Optional[str], name: Optional[str])
-

Bases: TikTokLive.types.objects.AbstractObject

-

User badges (e.g moderator)

-
-
- name: Optional[str]
-

The name for the badge

-
-
- -
-
- type: Optional[str]
-

The type of badge

-
-
- -
-
- -
-
- class TikTokLive.types.objects.BadgeContainer(imageBadges: typing.List[TikTokLive.types.objects.ImageBadge] = <factory>, badges: typing.List[TikTokLive.types.objects.Badge] = <factory>)
-

Bases: TikTokLive.types.objects.AbstractObject

-

Badge container housing a list of user badges

-
-
- badges: List[TikTokLive.types.objects.Badge]
-

A list of text badges the user has (e.g. Moderator/Friend badge)

-
-
- -
-
- imageBadges: List[TikTokLive.types.objects.ImageBadge]
-

A list of image badges the user has (e.g. Subscriber badge)

-
-
- -
-
- -
-
- class TikTokLive.types.objects.Emote(emoteId: Optional[str], image: Optional[TikTokLive.types.objects.EmoteImage]) -
-

Bases: object

-

The Emote a user sent in the chat

-
-
- emoteId: Optional[str]
-

ID of the TikTok Emote

-
-
- -
-
- image: Optional[TikTokLive.types.objects.EmoteImage]
-

Container encapsulating the image URL for the sent Emote

-
-
- -
-
- -
-
- class TikTokLive.types.objects.EmoteImage(imageUrl: Optional[str])
-

Bases: object

-

Container encapsulating the image URL for the Emote

-
-
- imageUrl: Optional[str]
-

TikTok CDN link to the given Emote for the streamer

-
-
- -
-
- -
-
- class TikTokLive.types.objects.ExtendedGift(id: Optional[int], name: Optional[str], type: Optional[int], diamond_count: Optional[int], describe: Optional[str], duration: Optional[int], event_name: Optional[str], icon: Optional[TikTokLive.types.objects.GiftIcon], image: Optional[TikTokLive.types.objects.GiftIcon], notify: Optional[bool], is_broadcast_gift: Optional[bool], is_displayed_on_panel: Optional[bool], is_effect_befview: Optional[bool], is_random_gift: Optional[bool], is_gray: Optional[bool])
-

Bases: TikTokLive.types.objects.AbstractObject

-

Extended gift data for a gift including a whole lotta extra properties.

-
-
- describe: Optional[str]
-
-
- -
-
- diamond_count: Optional[int]
-

The currency (Diamond) value of the item

-
-
- -
-
- duration: Optional[int]
-
-
- -
-
- event_name: Optional[str]
-
-
- -
-
- icon: Optional[TikTokLive.types.objects.GiftIcon]
-
-
- -
-
- id: Optional[int]
-

The ID of the gift

-
-
- -
-
- image: Optional[TikTokLive.types.objects.GiftIcon] -
-
-
- -
-
- is_broadcast_gift: Optional[bool]
-
-
- -
-
- is_displayed_on_panel: Optional[bool] -
-
-
- -
-
- is_effect_befview: Optional[bool]
-
-
- -
-
- is_gray: Optional[bool]
-
-
- -
-
- is_random_gift: Optional[bool]
-
-
- -
-
- name: Optional[str]
-

The name of the gift

-
-
- -
-
- notify: Optional[bool]
-
-
- -
-
- type: Optional[int]
-

The type of gift

-
-
- -
-
- -
-
- class TikTokLive.types.objects.ExtraAttributes(followRole: typing.Optional[int] = <factory>)
-

Bases: TikTokLive.types.objects.AbstractObject

-

Extra attributes on the User Object (e.g. following status)

-
-
- followRole: Optional[int]
-
-
- -
-
- -
-
- class TikTokLive.types.objects.FFmpegWrapper(runtime: Optional[str], thread: threading.Thread, ffmpeg: ffmpy.FFmpeg, verbose: bool, path: str, started_at: int = - 1)
-

Bases: object

-

A wrapper for the FFmpeg Stream Download utility in the TikTokLive Package

-
-
- ffmpeg: ffmpy.FFmpeg
-

The ffmpy FFmpeg object in which a subprocess is spawned to download

-
-
- -
-
- path: str
-

The path to download the video to

-
-
- -
-
- runtime: Optional[str]
-

FFMpeg argument for how long to download for

-
-
- -
-
- started_at: int = -1
-

The time at which the download began

-
-
- -
-
- thread: threading.Thread
-

The thread object in which a download is occuring

-
-
- -
-
- verbose: bool
-

Whether to include logging messages about the status of the download

-
-
- -
-
- -
-
- class TikTokLive.types.objects.Gift(giftId: Optional[int], repeatCount: Optional[int], repeatEnd: Optional[int], giftDetails: Optional[TikTokLive.types.objects.GiftDetails], giftExtra: Optional[TikTokLive.types.objects.GiftExtra], extended_gift: Optional[TikTokLive.types.objects.ExtendedGift]) -
-

Bases: TikTokLive.types.objects.AbstractObject

-

Gift object containing information about a given gift

-
-
- extended_gift: Optional[TikTokLive.types.objects.ExtendedGift] -
-

Extended gift including extra data (not very important as of april 2022)

-
-
- -
-
- giftDetails: Optional[TikTokLive.types.objects.GiftDetails]
-

Details about the specific Gift sent

-
-
- -
-
- giftExtra: Optional[TikTokLive.types.objects.GiftExtra]
-

Details like who the gift was sent to (multi-user streams)

-
-
- -
-
- giftId: Optional[int]
-

The Internal TikTok ID of the gift

-
-
- -
-
- property gift_type: int -
-

Alias for the giftDetails.giftType for backwards compatibility

-
-
Returns
-

giftType Value

-
-
-
-
- -
-
- repeatCount: Optional[int]
-

Number of times the gift has repeated

-
-
- -
-
- repeatEnd: Optional[int]
-

Whether or not the repetition is over

-
-
- -
-
- property repeat_count: int -
-

Alias for repeatCount for backwards compatibility

-
-
Returns
-

repeatCount Value

-
-
-
-
- -
-
- property repeat_end: int -
-

Alias for repeatEnd for backwards compatibility

-
-
Returns
-

repeatEnd Value

-
-
-
-
- -
-
- property streakable: bool -
-

Whether a given gift can have a streak

-
-
Returns
-

True if it is type 1, otherwise False

-
-
-
-
- -
-
- property streaking: bool -
-

Whether the streak is over

-
-
Returns
-

True if currently streaking, False if not

-
-
-
-
- -
-
- -
-
- class TikTokLive.types.objects.GiftDetailImage(giftPictureUrl: Optional[str])
-

Bases: TikTokLive.types.objects.AbstractObject

-

Gift image

-
-
- giftPictureUrl: Optional[str]
-

Icon URL for the Gift

-
-
- -
-
- -
-
- class TikTokLive.types.objects.GiftDetails(giftImage: Optional[TikTokLive.types.objects.GiftDetailImage], describe: Optional[str], giftType: Optional[int], diamondCount: Optional[int], giftName: Optional[str])
-

Bases: TikTokLive.types.objects.AbstractObject

-

Details about a given gift

-
-
- describe: Optional[str]
-

Describes the gift

-
-
- -
-
- diamondCount: Optional[int]
-

Diamond value of 1 of the gift

-
-
- -
-
- giftImage: Optional[TikTokLive.types.objects.GiftDetailImage] -
-

Image container for the Gift

-
-
- -
-
- giftName: Optional[str]
-

Name of the gift

-
-
- -
-
- giftType: Optional[int]
-

The type of gift. Type 1 are repeatable, any other type are not.

-
-
- -
-
- -
-
- class TikTokLive.types.objects.GiftExtra(timestamp: Optional[int], receiverUserId: Optional[int])
-

Bases: object

-

Gift object containing information about the gift recipient

-
-
- receiverUserId: Optional[int]
-

The user that received the gift

-
-
- -
-
- timestamp: Optional[int]
-

The time the gift was sent

-
-
- -
-
- -
-
- class TikTokLive.types.objects.GiftIcon(avg_color: Optional[str], uri: Optional[str], is_animated: Optional[bool], url_list: Optional[List[str]])
-

Bases: TikTokLive.types.objects.AbstractObject

-

Icon data for a given gift (such as its image URL)

-
-
- avg_color: Optional[str]
-
-
- -
-
- is_animated: Optional[bool]
-

Whether or not it is an animated icon

-
-
- -
-
- uri: Optional[str]
-
-
- -
-
- url_list: Optional[List[str]] -
-

A list of URLs containing various sizes of the gift’s icon

-
-
- -
-
- -
-
- class TikTokLive.types.objects.ImageBadge(displayType: Optional[int], image: Optional[TikTokLive.types.objects.ImageBadgeImage])
-

Bases: object

-

” - Image Badge object containing an image badge for a TikTok User

-
-
- displayType: Optional[int]
-

The displayType of the badge

-
-
- -
-
- image: Optional[TikTokLive.types.objects.ImageBadgeImage]
-

Container for the image badge

-
-
- -
-
- -
-
- class TikTokLive.types.objects.ImageBadgeImage(url: Optional[str])
-

Bases: object

-

Image container with the URL of the user badge

-
-
- url: Optional[str]
-

The TikTok CDN Image URL for the badge

-
-
- -
-
- -
-
- class TikTokLive.types.objects.LinkUser(userId: Optional[int], nickname: Optional[str], profilePicture: Optional[TikTokLive.types.objects.Avatar], uniqueId: Optional[str])
-

Bases: object

-

A user in a TikTok LinkMicBattle (TikTok Battle Events)

-
-
- nickname: Optional[str]
-

User’s Nickname

-
-
- -
-
- profilePicture: Optional[TikTokLive.types.objects.Avatar]
-

User’s Profile Picture

-
-
- -
-
- uniqueId: Optional[str]
-

The uniqueId of the user

-
-
- -
-
- userId: Optional[int]
-

userId of the user

-
-
- -
-
- -
-
- class TikTokLive.types.objects.MemberMessage(eventDetails: Optional[TikTokLive.types.objects.MemberMessageDetails])
-

Bases: object

-

Container encapsulating the member message details

-
-
- eventDetails: Optional[TikTokLive.types.objects.MemberMessageDetails] -
-
-
- -
-
- -
-
- class TikTokLive.types.objects.MemberMessageDetails(displayType: Optional[str], label: Optional[str])
-

Bases: object

-

Details about a given member message proto event

-
-
- displayType: Optional[str]
-

The displayType of the message corresponding to the type of member message

-
-
- -
-
- label: Optional[str]
-

Display Label for the member message

-
-
- -
-
- -
-
- class TikTokLive.types.objects.MicArmiesGroup(points: typing.Optional[int], users: typing.List[TikTokLive.types.objects.User] - = <factory>)
-

Bases: object

-

A group containing

-
-
- points: Optional[int]
-

The number of points the person has

-
-
- -
-
- users: List[TikTokLive.types.objects.User] -
-

(Presumably) the users involved in the battle

-
-
- -
-
- -
-
- class TikTokLive.types.objects.MicArmiesUser(hostUserId: Optional[int], battleGroups: Optional[TikTokLive.types.objects.MicArmiesGroup])
-

Bases: object

-

Information about the Mic Armies User

-
-
- battleGroups: Optional[TikTokLive.types.objects.MicArmiesGroup]
-

Information about the users involved in the battle

-
-
- -
-
- hostUserId: Optional[int]
-

The user ID of the TikTok host

-
-
- -
-
- -
-
- class TikTokLive.types.objects.MicBattleGroup(user: TikTokLive.types.objects.LinkUser)
-

Bases: object

-

A container encapsulating LinkUser data for TikTok Battles

-
-
- user: TikTokLive.types.objects.LinkUser
-

The TikTok battle LinkUser

-
-
- -
-
- -
-
- class TikTokLive.types.objects.MicBattleUser(battleGroup: TikTokLive.types.objects.MicBattleGroup)
-

Bases: object

-

A container encapsulating the LinkUser data for TikTok Battles

-
-
- battleGroup: TikTokLive.types.objects.MicBattleGroup
-
-
- -
-
- -
-
- class TikTokLive.types.objects.RankContainer(rankings: Optional[TikTokLive.types.objects.WeeklyRanking])
-

Bases: object

-

Container encapsulating weekly ranking data

-
-
- rankings: Optional[TikTokLive.types.objects.WeeklyRanking] -
-
-
- -
-
- -
-
- class TikTokLive.types.objects.RankItem(colour: Optional[str], id: Optional[int])
-

Bases: object

-

Rank Item for the user ranking

-
-
- colour: Optional[str]
-

Colour that the rank corresponds to (for the UI)

-
-
- -
-
- id: Optional[int]
-

The rank. If id=400, they are in the Top 400

-
-
- -
-
- -
-
- class TikTokLive.types.objects.TreasureBoxData(coins: Optional[int], canOpen: Optional[int], timestamp: Optional[int])
-

Bases: object

-

Information about the gifted treasure box

-
-
- canOpen: Optional[int]
-

Whether the treasure box can be opened

-
-
- -
-
- coins: Optional[int]
-

Coins of the treasure box

-
-
- -
-
- timestamp: Optional[int]
-

Timestamp for when the treasure box was sent

-
-
- -
-
- -
-
- class TikTokLive.types.objects.User(userId: typing.Optional[int], uniqueId: typing.Optional[str], nickname: typing.Optional[str], profilePicture: typing.Optional[TikTokLive.types.objects.Avatar], extraAttributes: TikTokLive.types.objects.ExtraAttributes - = <factory>, badges: typing.List[TikTokLive.types.objects.BadgeContainer] - = <factory>)
-

Bases: TikTokLive.types.objects.AbstractObject

-

User object containing information on a TikTok User

-
-
- badges: List[TikTokLive.types.objects.BadgeContainer]
-

Badges for the user containing information such as if they are a stream moderator

-
-
- -
-
- extraAttributes: TikTokLive.types.objects.ExtraAttributes
-

Extra attributes for the user such as if they are following the streamer

-
-
- -
-
- property is_following: bool -
-

Whether they are following the watched streamer

-
-
- -
-
- property is_friend: bool -
-

Whether they are a friend of the watched streamer

-
-
- -
-
- property is_moderator: bool -
-

Whether they are a moderator for the watched streamer

-
-
- -
-
- property is_new_gifter: bool -
-

Whether they are a new gifter in the streamer’s stream

-
-
- -
-
- property is_subscriber: bool -
-

Whether they are a subscriber in the watched stream

-
-
- -
-
- nickname: Optional[str]
-

The user’s nickname (e.g Charlie d’Amelio)

-
-
- -
-
- profilePicture: Optional[TikTokLive.types.objects.Avatar]
-

An object containing avatar url information

-
-
- -
-
- property top_gifter_rank: Optional[int]
-

Their top gifter rank if they are a top gifter

-
-
- -
-
- uniqueId: Optional[str]
-

The user’s uniqueId (e.g @charlidamelio)

-
-
- -
-
- userId: Optional[int]
-

The user’s user id

-
-
- -
-
- -
-
- class TikTokLive.types.objects.WeeklyRanking(type: Optional[str], label: Optional[str], rank: Optional[TikTokLive.types.objects.RankItem]) -
-

Bases: object

-

Container with the weekly ranking data

-
-
- label: Optional[str]
-

Label for the UI

-
-
- -
-
- rank: Optional[TikTokLive.types.objects.RankItem] -
-

The weekly ranking data

-
-
- -
-
- type: Optional[str]
-

Unknown

-
-
- -
-
- -
-
- -

Module contents

-
-
- - -
-
- -
+
+ +
+
+
+ +
+
+
+
+ +
+

TikTokLive.types package

+
+

Submodules

+
+
+

TikTokLive.types.errors module

+
+
+exception TikTokLive.types.errors.AlreadyConnected
+

Bases: RuntimeError

+

Error that is raised when attempting to connect to a livestream whilst already connected.

+
+ +
+
+exception TikTokLive.types.errors.AlreadyConnecting
+

Bases: RuntimeError

+

Error that is raised when attempting to connect to a livestream whilst already attempting to connect.

+
+ +
+
+exception TikTokLive.types.errors.AlreadyDownloadingStream
+

Bases: DownloadStreamError

+

Error raised when already downloading a livestream and one attempts to start a second download

+
+ +
+
+exception TikTokLive.types.errors.ChatMessageRepeat
+

Bases: ChatMessageSendFailure

+

Error raised when someone repeats a chat message

+
+ +
+
+exception TikTokLive.types.errors.ChatMessageSendFailure
+

Bases: RuntimeError

+

Error raised when a TikTok chat message fails to send

+
+ +
+
+exception TikTokLive.types.errors.DownloadProcessNotFound
+

Bases: DownloadStreamError

+

Error raised when stopping a download and the process is not found. Usually, you’re stopping it before the process spawns

+
+ +
+
+exception TikTokLive.types.errors.DownloadStreamError
+

Bases: RuntimeError

+

Error raised for anything relating to downloading streams

+
+ +
+
+exception TikTokLive.types.errors.FailedConnection
+

Bases: RuntimeError

+

Error that is raised when the connection to a livestream fails (generic).

+
+ +
+
+exception TikTokLive.types.errors.FailedFetchGifts
+

Bases: FailedHTTPRequest

+

Error raised when fetching gifts encounters an exception

+
+ +
+
+exception TikTokLive.types.errors.FailedFetchRoomInfo
+

Bases: FailedHTTPRequest

+

Error raised when failing to fetch room info

+
+ +
+
+exception TikTokLive.types.errors.FailedHTTPRequest
+

Bases: RuntimeError

+

Error raised whenever a request fails to HTTP [Generic]

+
+ +
+
+exception TikTokLive.types.errors.FailedRoomPolling
+

Bases: FailedHTTPRequest

+

Error raised when room polling encounters an exception

+
+ +
+
+exception TikTokLive.types.errors.InitialCursorMissing
+

Bases: FailedConnection

+

Error that is raised when the initial cursor is missing

+
+ +
+
+exception TikTokLive.types.errors.InvalidSessionId
+

Bases: RuntimeError

+

Error raised when a session ID is expired or missing

+
+ +
+
+exception TikTokLive.types.errors.LiveNotFound
+

Bases: RuntimeError

+

Error that is raised when the livestream you are trying to connect to is offline/does-not-exist.

+
+ +
+
+exception TikTokLive.types.errors.NotDownloadingStream
+

Bases: DownloadStreamError

+

Error raised when trying to stop downloading a livestream you are not currently downloading

+
+ +
+
+exception TikTokLive.types.errors.WebsocketConnectionFailed
+

Bases: RuntimeError

+

Raised when a connection to the TikTok Webcast websocket fails

+
+ +
+
+

TikTokLive.types.events module

+
+
+class TikTokLive.types.events.AbstractEvent(data: dict = {})
+

Bases: object

+

Abstract Event

+
+
+property as_dict: dict
+

Return a copy of the object as a dictionary

+
+
Returns
+

A copy of the raw payload

+
+
+
+ +
+
+name: str = 'event'
+
+ +
+
+set_as_dict(data: dict)
+

Set that _as_dict attribute

+
+
Parameters
+

data – The data to set it to

+
+
Returns
+

None

+
+
+
+ +
+ +
+
+class TikTokLive.types.events.CommentEvent(user: Optional[User], comment: Optional[str], name: str = 'comment')
+

Bases: AbstractEvent

+

Event that fires when someone comments on the livestream

+
+
+comment: Optional[str]
+

The UTF-8 text comment that was sent

+
+ +
+
+name: str = 'comment'
+
+ +
+
+user: Optional[User]
+

The user that sent the comment

+
+ +
+ +
+
+class TikTokLive.types.events.ConnectEvent(name: str = 'connect')
+

Bases: AbstractEvent

+

Event that fires when the client connect to a livestream

+
+
+name: str = 'connect'
+
+ +
+ +
+
+class TikTokLive.types.events.DisconnectEvent(name: str = 'disconnect')
+

Bases: AbstractEvent

+

Event that fires when the client disconnects from a livestream

+
+
+name: str = 'disconnect'
+
+ +
+ +
+
+class TikTokLive.types.events.EmoteEvent(user: Optional[User], emote: Optional[Emote], name: str = 'emote')
+

Bases: AbstractEvent

+

Event that fires when someone sends a subscriber emote

+
+
+emote: Optional[Emote]
+

The emote the person sent

+
+ +
+
+name: str = 'emote'
+
+ +
+
+user: Optional[User]
+

Person who sent the emote message

+
+ +
+ +
+
+class TikTokLive.types.events.EnvelopeEvent(treasureBoxData: Optional[TreasureBoxData], treasureBoxUser: Optional[User], name: str = 'envelope')
+

Bases: AbstractEvent

+

Event that fire when someone sends an envelope

+
+
+name: str = 'envelope'
+
+ +
+
+treasureBoxData: Optional[TreasureBoxData]
+

Data about the enclosed Treasure Box in the envelope

+
+ +
+
+treasureBoxUser: Optional[User]
+

Data about the user that sent the treasure box

+
+ +
+ +
+
+class TikTokLive.types.events.FollowEvent(user: Optional[User], displayType: Optional[str], label: Optional[str], name: str = 'follow')
+

Bases: AbstractEvent

+

Event that fires when a user follows the livestream

+
+
+displayType: Optional[str]
+
+ +
+
+label: Optional[str]
+
+ +
+
+name: str = 'follow'
+
+ +
+
+user: Optional[User]
+

The user that followed the streamer

+
+ +
+ +
+
+class TikTokLive.types.events.GiftEvent(user: Optional[User], gift: Optional[Gift], name: str = 'gift')
+

Bases: AbstractEvent

+

Event that fires when a gift is received

+
+
+gift: Optional[Gift]
+

Object containing gift data

+
+ +
+
+name: str = 'gift'
+
+ +
+
+user: Optional[User]
+

The user that sent the gift

+
+ +
+ +
+
+class TikTokLive.types.events.JoinEvent(user: Optional[User], displayType: Optional[str], label: Optional[str], name: str = 'join')
+

Bases: AbstractEvent

+

Event that fires when a user joins the livestream

+
+
+displayType: Optional[str]
+

The type of event

+
+ +
+
+label: Optional[str]
+

Label for event in live chat

+
+ +
+
+name: str = 'join'
+
+ +
+
+property through_share: bool
+

Whether they joined through a link vs. the TikTok App

+
+
Returns
+

Returns True if they joined through a share link

+
+
+
+ +
+
+user: Optional[User]
+

The user that joined the stream

+
+ +
+ +
+
+class TikTokLive.types.events.LikeEvent(user: Optional[User], likeCount: Optional[int], totalLikeCount: Optional[int], displayType: Optional[str], label: Optional[str], name: str = 'like')
+

Bases: AbstractEvent

+

Event that fires when a user likes the livestream

+
+
+displayType: Optional[str]
+
+ +
+
+label: Optional[str]
+
+ +
+
+likeCount: Optional[int]
+

The number of likes they sent (I think?)

+
+ +
+
+name: str = 'like'
+
+ +
+
+totalLikeCount: Optional[int]
+

The total number of likes on the stream

+
+ +
+
+user: Optional[User]
+

The user that liked the stream

+
+ +
+ +
+
+class TikTokLive.types.events.LiveEndEvent(name: str = 'live_end')
+

Bases: AbstractEvent

+

Event that fires when the livestream ends

+
+
+name: str = 'live_end'
+
+ +
+ +
+
+class TikTokLive.types.events.MicArmiesEvent(battleStatus: ~typing.Optional[int], battleUsers: ~typing.List[~TikTokLive.types.objects.MicArmiesUser] = <factory>, name: str = 'mic_armies')
+

Bases: AbstractEvent

+

Event that fires during a Mic Battle to update its progress

+
+
+battleStatus: Optional[int]
+

The status of the current Battle

+
+ +
+
+battleUsers: List[MicArmiesUser]
+

Information about the users engaged in the Mic Battle

+
+ +
+
+name: str = 'mic_armies'
+
+ +
+ +
+
+class TikTokLive.types.events.MicBattleEvent(battleUsers: ~typing.List[~TikTokLive.types.objects.MicBattleUser] = <factory>, name: str = 'mic_battle')
+

Bases: AbstractEvent

+

Event that fires when a Mic Battle starts

+
+
+battleUsers: List[MicBattleUser]
+

Information about the users engaged in the Mic Battle

+
+ +
+
+name: str = 'mic_battle'
+
+ +
+ +
+
+class TikTokLive.types.events.MoreShareEvent(user: Optional[User], displayType: Optional[str], label: Optional[str], name: str = 'more_share')
+

Bases: ShareEvent

+

Event that fires when a user shared the livestream more than 5 users or more than 10 users

+

“user123 has shared to more than 10 people!”

+
+
+property amount: Optional[int]
+

The number of people that have joined the stream off the user

+
+
Returns
+

The number of people that have joined

+
+
+
+ +
+
+name: str = 'more_share'
+
+ +
+ +
+
+class TikTokLive.types.events.QuestionEvent(questionText: Optional[str], user: Optional[User], name: str = 'question')
+

Bases: AbstractEvent

+

Event that fires when someone asks a Q&A question

+
+
+name: str = 'question'
+
+ +
+
+questionText: Optional[str]
+

The question that was asked

+
+ +
+
+user: Optional[User]
+

User who asked the question

+
+ +
+ +
+
+class TikTokLive.types.events.ShareEvent(user: Optional[User], displayType: Optional[str], label: Optional[str], name: str = 'share')
+

Bases: AbstractEvent

+

Event that fires when a user shares the livestream

+
+
+displayType: Optional[str]
+

Type of event

+
+ +
+
+label: Optional[str]
+

Internal Webcast Label

+
+ +
+
+name: str = 'share'
+
+ +
+
+user: Optional[User]
+

The user that shared the stream

+
+ +
+ +
+
+class TikTokLive.types.events.SubscribeEvent(user: Optional[User], actionId: Optional[int], event: Optional[MemberMessage], name: str = 'subscribe')
+

Bases: AbstractEvent

+

Event that fires when someone subscribes to the streamer

+
+
+actionId: Optional[int]
+

The actionId of the MemberMessage corresponding to a sub (actionId=7)

+
+ +
+
+event: Optional[MemberMessage]
+

The details of the MemberMessage resulting in a subscription

+
+ +
+
+name: str = 'subscribe'
+
+ +
+
+user: Optional[User]
+

The user that subscribed to the streamer

+
+ +
+ +
+
+class TikTokLive.types.events.UnknownEvent(name: str = 'unknown')
+

Bases: AbstractEvent

+

Event that fires when an event is received that is not handled by other events in the library.

+
+
+name: str = 'unknown'
+
+ +
+ +
+
+class TikTokLive.types.events.ViewerCountUpdateEvent(viewerCount: Optional[int], name: str = 'viewer_count_update')
+

Bases: AbstractEvent

+

Event that fires when the viewer count for the livestream updates

+
+
+name: str = 'viewer_count_update'
+
+ +
+
+viewerCount: Optional[int]
+

The number of people viewing the stream currently

+
+ +
+ +
+
+class TikTokLive.types.events.WeeklyRankingEvent(data: Optional[RankContainer], name: str = 'weekly_ranking')
+

Bases: AbstractEvent

+

Event that fires when the weekly rankings are updated

+
+
+data: Optional[RankContainer]
+

Weekly ranking data

+
+ +
+
+name: str = 'weekly_ranking'
+
+ +
+ +
+
+

TikTokLive.types.objects module

+
+
+class TikTokLive.types.objects.AbstractObject
+

Bases: object

+

Abstract Object

+
+ +
+
+class TikTokLive.types.objects.Avatar(urls: List[str])
+

Bases: AbstractObject

+

The URLs to the avatar of a TikTok User

+
+
+property avatar_url
+

The last (highest quality) avatar URL supplied

+
+ +
+
+urls: List[str]
+
+ +
+ +
+
+class TikTokLive.types.objects.Badge(type: Optional[str], name: Optional[str])
+

Bases: AbstractObject

+

User badges (e.g moderator)

+
+
+name: Optional[str]
+

The name for the badge

+
+ +
+
+type: Optional[str]
+

The type of badge

+
+ +
+ +
+
+class TikTokLive.types.objects.BadgeContainer(imageBadges: ~typing.List[~TikTokLive.types.objects.ImageBadge] = <factory>, badges: ~typing.List[~TikTokLive.types.objects.Badge] = <factory>)
+

Bases: AbstractObject

+

Badge container housing a list of user badges

+
+
+badges: List[Badge]
+

A list of text badges the user has (e.g. Moderator/Friend badge)

+
+ +
+
+imageBadges: List[ImageBadge]
+

A list of image badges the user has (e.g. Subscriber badge)

+
+ +
+ +
+
+class TikTokLive.types.objects.Emote(emoteId: Optional[str], image: Optional[EmoteImage])
+

Bases: object

+

The Emote a user sent in the chat

+
+
+emoteId: Optional[str]
+

ID of the TikTok Emote

+
+ +
+
+image: Optional[EmoteImage]
+

Container encapsulating the image URL for the sent Emote

+
+ +
+ +
+
+class TikTokLive.types.objects.EmoteImage(imageUrl: Optional[str])
+

Bases: object

+

Container encapsulating the image URL for the Emote

+
+
+imageUrl: Optional[str]
+

TikTok CDN link to the given Emote for the streamer

+
+ +
+ +
+
+class TikTokLive.types.objects.ExtendedGift(id: Optional[int], name: Optional[str], type: Optional[int], diamond_count: Optional[int], describe: Optional[str], duration: Optional[int], event_name: Optional[str], icon: Optional[GiftIcon], image: Optional[GiftIcon], notify: Optional[bool], is_broadcast_gift: Optional[bool], is_displayed_on_panel: Optional[bool], is_effect_befview: Optional[bool], is_random_gift: Optional[bool], is_gray: Optional[bool])
+

Bases: AbstractObject

+

Extended gift data for a gift including a whole lotta extra properties.

+
+
+describe: Optional[str]
+
+ +
+
+diamond_count: Optional[int]
+

The currency (Diamond) value of the item

+
+ +
+
+duration: Optional[int]
+
+ +
+
+event_name: Optional[str]
+
+ +
+
+icon: Optional[GiftIcon]
+
+ +
+
+id: Optional[int]
+

The ID of the gift

+
+ +
+
+image: Optional[GiftIcon]
+
+ +
+
+is_broadcast_gift: Optional[bool]
+
+ +
+
+is_displayed_on_panel: Optional[bool]
+
+ +
+
+is_effect_befview: Optional[bool]
+
+ +
+
+is_gray: Optional[bool]
+
+ +
+
+is_random_gift: Optional[bool]
+
+ +
+
+name: Optional[str]
+

The name of the gift

+
+ +
+
+notify: Optional[bool]
+
+ +
+
+type: Optional[int]
+

The type of gift

+
+ +
+ +
+
+class TikTokLive.types.objects.ExtraAttributes(followRole: ~typing.Optional[int] = <factory>)
+

Bases: AbstractObject

+

Extra attributes on the User Object (e.g. following status)

+
+
+followRole: Optional[int]
+
+ +
+ +
+
+class TikTokLive.types.objects.FFmpegWrapper(runtime: Optional[str], thread: Thread, ffmpeg: FFmpeg, verbose: bool, path: str, started_at: int = - 1)
+

Bases: object

+

A wrapper for the FFmpeg Stream Download utility in the TikTokLive Package

+
+
+ffmpeg: FFmpeg
+

The ffmpy FFmpeg object in which a subprocess is spawned to download

+
+ +
+
+path: str
+

The path to download the video to

+
+ +
+
+runtime: Optional[str]
+

FFMpeg argument for how long to download for

+
+ +
+
+started_at: int = -1
+

The time at which the download began

+
+ +
+
+thread: Thread
+

The thread object in which a download is occuring

+
+ +
+
+verbose: bool
+

Whether to include logging messages about the status of the download

+
+ +
+ +
+
+class TikTokLive.types.objects.Gift(giftId: Optional[int], repeatCount: Optional[int], repeatEnd: Optional[int], giftDetails: Optional[GiftDetails], giftExtra: Optional[GiftExtra], extended_gift: Optional[ExtendedGift])
+

Bases: AbstractObject

+

Gift object containing information about a given gift

+
+
+extended_gift: Optional[ExtendedGift]
+

Extended gift including extra data (not very important as of april 2022)

+
+ +
+
+giftDetails: Optional[GiftDetails]
+

Details about the specific Gift sent

+
+ +
+
+giftExtra: Optional[GiftExtra]
+

Details like who the gift was sent to (multi-user streams)

+
+ +
+
+giftId: Optional[int]
+

The Internal TikTok ID of the gift

+
+ +
+
+property gift_type: int
+

Alias for the giftDetails.giftType for backwards compatibility

+
+
Returns
+

giftType Value

+
+
+
+ +
+
+repeatCount: Optional[int]
+

Number of times the gift has repeated

+
+ +
+
+repeatEnd: Optional[int]
+

Whether or not the repetition is over

+
+ +
+
+property repeat_count: int
+

Alias for repeatCount for backwards compatibility

+
+
Returns
+

repeatCount Value

+
+
+
+ +
+
+property repeat_end: int
+

Alias for repeatEnd for backwards compatibility

+
+
Returns
+

repeatEnd Value

+
+
+
+ +
+
+property streakable: bool
+

Whether a given gift can have a streak

+
+
Returns
+

True if it is type 1, otherwise False

+
+
+
+ +
+
+property streaking: bool
+

Whether the streak is over

+
+
Returns
+

True if currently streaking, False if not

+
+
+
+ +
+ +
+
+class TikTokLive.types.objects.GiftDetailImage(giftPictureUrl: Optional[str])
+

Bases: AbstractObject

+

Gift image

+
+
+giftPictureUrl: Optional[str]
+

Icon URL for the Gift

+
+ +
+ +
+
+class TikTokLive.types.objects.GiftDetails(giftImage: Optional[GiftDetailImage], describe: Optional[str], giftType: Optional[int], diamondCount: Optional[int], giftName: Optional[str])
+

Bases: AbstractObject

+

Details about a given gift

+
+
+describe: Optional[str]
+

Describes the gift

+
+ +
+
+diamondCount: Optional[int]
+

Diamond value of 1 of the gift

+
+ +
+
+giftImage: Optional[GiftDetailImage]
+

Image container for the Gift

+
+ +
+
+giftName: Optional[str]
+

Name of the gift

+
+ +
+
+giftType: Optional[int]
+

The type of gift. Type 1 are repeatable, any other type are not.

+
+ +
+ +
+
+class TikTokLive.types.objects.GiftExtra(timestamp: Optional[int], receiverUserId: Optional[int])
+

Bases: object

+

Gift object containing information about the gift recipient

+
+
+receiverUserId: Optional[int]
+

The user that received the gift

+
+ +
+
+timestamp: Optional[int]
+

The time the gift was sent

+
+ +
+ +
+
+class TikTokLive.types.objects.GiftIcon(avg_color: Optional[str], uri: Optional[str], is_animated: Optional[bool], url_list: Optional[List[str]])
+

Bases: AbstractObject

+

Icon data for a given gift (such as its image URL)

+
+
+avg_color: Optional[str]
+
+ +
+
+is_animated: Optional[bool]
+

Whether or not it is an animated icon

+
+ +
+
+uri: Optional[str]
+
+ +
+
+url_list: Optional[List[str]]
+

A list of URLs containing various sizes of the gift’s icon

+
+ +
+ +
+
+class TikTokLive.types.objects.ImageBadge(displayType: Optional[int], image: Optional[ImageBadgeImage])
+

Bases: object

+

” +Image Badge object containing an image badge for a TikTok User

+
+
+displayType: Optional[int]
+

The displayType of the badge

+
+ +
+
+image: Optional[ImageBadgeImage]
+

Container for the image badge

+
+ +
+ +
+
+class TikTokLive.types.objects.ImageBadgeImage(url: Optional[str])
+

Bases: object

+

Image container with the URL of the user badge

+
+
+url: Optional[str]
+

The TikTok CDN Image URL for the badge

+
+ +
+ +
+
+class TikTokLive.types.objects.LinkUser(userId: Optional[int], nickname: Optional[str], profilePicture: Optional[Avatar], uniqueId: Optional[str])
+

Bases: object

+

A user in a TikTok LinkMicBattle (TikTok Battle Events)

+
+
+nickname: Optional[str]
+

User’s Nickname

+
+ +
+
+profilePicture: Optional[Avatar]
+

User’s Profile Picture

+
+ +
+
+uniqueId: Optional[str]
+

The uniqueId of the user

+
+ +
+
+userId: Optional[int]
+

userId of the user

+
+ +
+ +
+
+class TikTokLive.types.objects.MemberMessage(eventDetails: Optional[MemberMessageDetails])
+

Bases: object

+

Container encapsulating the member message details

+
+
+eventDetails: Optional[MemberMessageDetails]
+
+ +
+ +
+
+class TikTokLive.types.objects.MemberMessageDetails(displayType: Optional[str], label: Optional[str])
+

Bases: object

+

Details about a given member message proto event

+
+
+displayType: Optional[str]
+

The displayType of the message corresponding to the type of member message

+
+ +
+
+label: Optional[str]
+

Display Label for the member message

+
+ +
+ +
+
+class TikTokLive.types.objects.MicArmiesGroup(points: ~typing.Optional[int], users: ~typing.List[~TikTokLive.types.objects.User] = <factory>)
+

Bases: object

+

A group containing

+
+
+points: Optional[int]
+

The number of points the person has

+
+ +
+
+users: List[User]
+

(Presumably) the users involved in the battle

+
+ +
+ +
+
+class TikTokLive.types.objects.MicArmiesUser(hostUserId: Optional[int], battleGroups: Optional[MicArmiesGroup])
+

Bases: object

+

Information about the Mic Armies User

+
+
+battleGroups: Optional[MicArmiesGroup]
+

Information about the users involved in the battle

+
+ +
+
+hostUserId: Optional[int]
+

The user ID of the TikTok host

+
+ +
+ +
+
+class TikTokLive.types.objects.MicBattleGroup(user: LinkUser)
+

Bases: object

+

A container encapsulating LinkUser data for TikTok Battles

+
+
+user: LinkUser
+

The TikTok battle LinkUser

+
+ +
+ +
+
+class TikTokLive.types.objects.MicBattleUser(battleGroup: MicBattleGroup)
+

Bases: object

+

A container encapsulating the LinkUser data for TikTok Battles

+
+
+battleGroup: MicBattleGroup
+
+ +
+ +
+
+class TikTokLive.types.objects.RankContainer(rankings: Optional[WeeklyRanking])
+

Bases: object

+

Container encapsulating weekly ranking data

+
+
+rankings: Optional[WeeklyRanking]
+
+ +
+ +
+
+class TikTokLive.types.objects.RankItem(colour: Optional[str], id: Optional[int])
+

Bases: object

+

Rank Item for the user ranking

+
+
+colour: Optional[str]
+

Colour that the rank corresponds to (for the UI)

+
+ +
+
+id: Optional[int]
+

The rank. If id=400, they are in the Top 400

+
+ +
+ +
+
+class TikTokLive.types.objects.TreasureBoxData(coins: Optional[int], canOpen: Optional[int], timestamp: Optional[int])
+

Bases: object

+

Information about the gifted treasure box

+
+
+canOpen: Optional[int]
+

Whether the treasure box can be opened

+
+ +
+
+coins: Optional[int]
+

Coins of the treasure box

+
+ +
+
+timestamp: Optional[int]
+

Timestamp for when the treasure box was sent

+
+ +
+ +
+
+class TikTokLive.types.objects.User(userId: ~typing.Optional[int], uniqueId: ~typing.Optional[str], nickname: ~typing.Optional[str], profilePicture: ~typing.Optional[~TikTokLive.types.objects.Avatar], extraAttributes: ~TikTokLive.types.objects.ExtraAttributes = <factory>, badges: ~typing.List[~TikTokLive.types.objects.BadgeContainer] = <factory>)
+

Bases: AbstractObject

+

User object containing information on a TikTok User

+
+
+badges: List[BadgeContainer]
+

Badges for the user containing information such as if they are a stream moderator

+
+ +
+
+extraAttributes: ExtraAttributes
+

Extra attributes for the user such as if they are following the streamer

+
+ +
+
+property is_following: bool
+

Whether they are following the watched streamer

+
+ +
+
+property is_friend: bool
+

Whether they are a friend of the watched streamer

+
+ +
+
+property is_moderator: bool
+

Whether they are a moderator for the watched streamer

+
+ +
+
+property is_new_gifter: bool
+

Whether they are a new gifter in the streamer’s stream

+
+ +
+
+property is_subscriber: bool
+

Whether they are a subscriber in the watched stream

+
+ +
+
+nickname: Optional[str]
+

The user’s nickname (e.g Charlie d’Amelio)

+
+ +
+
+profilePicture: Optional[Avatar]
+

An object containing avatar url information

+
+ +
+
+property top_gifter_rank: Optional[int]
+

Their top gifter rank if they are a top gifter

+
+ +
+
+uniqueId: Optional[str]
+

The user’s uniqueId (e.g @charlidamelio)

+
+ +
+
+userId: Optional[int]
+

The user’s user id

+
+ +
+ +
+
+class TikTokLive.types.objects.WeeklyRanking(type: Optional[str], label: Optional[str], rank: Optional[RankItem])
+

Bases: object

+

Container with the weekly ranking data

+
+
+label: Optional[str]
+

Label for the UI

+
+ +
+
+rank: Optional[RankItem]
+

The weekly ranking data

+
+ +
+
+type: Optional[str]
+

Unknown

+
+ +
+ +
+
+

Module contents

+
+
+ + +
+
+
+
-
- +
+ \ No newline at end of file diff --git a/docs/_sources/TikTokLive.client.rst.txt b/docs/_sources/TikTokLive.client.rst.txt index 08e2d24..e6a5c1b 100644 --- a/docs/_sources/TikTokLive.client.rst.txt +++ b/docs/_sources/TikTokLive.client.rst.txt @@ -20,6 +20,14 @@ TikTokLive.client.client module :undoc-members: :show-inheritance: +TikTokLive.client.config module +------------------------------- + +.. automodule:: TikTokLive.client.config + :members: + :undoc-members: + :show-inheritance: + TikTokLive.client.http module ----------------------------- @@ -28,10 +36,10 @@ TikTokLive.client.http module :undoc-members: :show-inheritance: -TikTokLive.client.proxy module ------------------------------- +TikTokLive.client.websocket module +---------------------------------- -.. automodule:: TikTokLive.client.proxy +.. automodule:: TikTokLive.client.websocket :members: :undoc-members: :show-inheritance: diff --git a/docs/_static/_sphinx_javascript_frameworks_compat.js b/docs/_static/_sphinx_javascript_frameworks_compat.js new file mode 100644 index 0000000..8549469 --- /dev/null +++ b/docs/_static/_sphinx_javascript_frameworks_compat.js @@ -0,0 +1,134 @@ +/* + * _sphinx_javascript_frameworks_compat.js + * ~~~~~~~~~~ + * + * Compatability shim for jQuery and underscores.js. + * + * WILL BE REMOVED IN Sphinx 6.0 + * xref RemovedInSphinx60Warning + * + */ + +/** + * select a different prefix for underscore + */ +$u = _.noConflict(); + + +/** + * small helper function to urldecode strings + * + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL + */ +jQuery.urldecode = function(x) { + if (!x) { + return x + } + return decodeURIComponent(x.replace(/\+/g, ' ')); +}; + +/** + * small helper function to urlencode strings + */ +jQuery.urlencode = encodeURIComponent; + +/** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ +jQuery.getQueryParameters = function(s) { + if (typeof s === 'undefined') + s = document.location.search; + var parts = s.substr(s.indexOf('?') + 1).split('&'); + var result = {}; + for (var i = 0; i < parts.length; i++) { + var tmp = parts[i].split('=', 2); + var key = jQuery.urldecode(tmp[0]); + var value = jQuery.urldecode(tmp[1]); + if (key in result) + result[key].push(value); + else + result[key] = [value]; + } + return result; +}; + +/** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ +jQuery.fn.highlightText = function(text, className) { + function highlight(node, addItems) { + if (node.nodeType === 3) { + var val = node.nodeValue; + var pos = val.toLowerCase().indexOf(text); + if (pos >= 0 && + !jQuery(node.parentNode).hasClass(className) && + !jQuery(node.parentNode).hasClass("nohighlight")) { + var span; + var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.className = className; + } + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + node.parentNode.insertBefore(span, node.parentNode.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling)); + node.nodeValue = val.substr(0, pos); + if (isInSVG) { + var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + var bbox = node.parentElement.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute('class', className); + addItems.push({ + "parent": node.parentNode, + "target": rect}); + } + } + } + else if (!jQuery(node).is("button, select, textarea")) { + jQuery.each(node.childNodes, function() { + highlight(this, addItems); + }); + } + } + var addItems = []; + var result = this.each(function() { + highlight(this, addItems); + }); + for (var i = 0; i < addItems.length; ++i) { + jQuery(addItems[i].parent).before(addItems[i].target); + } + return result; +}; + +/* + * backward compatibility for jQuery.browser + * This will be supported until firefox bug is fixed. + */ +if (!jQuery.browser) { + jQuery.uaMatch = function(ua) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || + /(webkit)[ \/]([\w.]+)/.exec(ua) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || + /(msie) ([\w.]+)/.exec(ua) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; + }; + jQuery.browser = {}; + jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; +} diff --git a/docs/_static/basic.css b/docs/_static/basic.css index bf18350..7d5974c 100644 --- a/docs/_static/basic.css +++ b/docs/_static/basic.css @@ -222,7 +222,7 @@ table.modindextable td { /* -- general body styles --------------------------------------------------- */ div.body { - min-width: 450px; + min-width: 360px; max-width: 800px; } @@ -236,7 +236,6 @@ div.body p, div.body dd, div.body li, div.body blockquote { a.headerlink { visibility: hidden; } - a.brackets:before, span.brackets > a:before{ content: "["; @@ -247,6 +246,7 @@ span.brackets > a:after { content: "]"; } + h1:hover > a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, @@ -334,13 +334,11 @@ aside.sidebar { p.sidebar-title { font-weight: bold; } - div.admonition, div.topic, blockquote { clear: left; } /* -- topics ---------------------------------------------------------------- */ - div.topic { border: 1px solid #ccc; padding: 7px; @@ -428,10 +426,6 @@ table.docutils td, table.docutils th { border-bottom: 1px solid #aaa; } -table.footnote td, table.footnote th { - border: 0 !important; -} - th { text-align: left; padding-right: 5px; @@ -615,6 +609,7 @@ ul.simple p { margin-bottom: 0; } +/* Docutils 0.17 and older (footnotes & citations) */ dl.footnote > dt, dl.citation > dt { float: left; @@ -632,6 +627,33 @@ dl.citation > dd:after { clear: both; } +/* Docutils 0.18+ (footnotes & citations) */ +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +/* Footnotes & citations ends */ + dl.field-list { display: grid; grid-template-columns: fit-content(30%) auto; diff --git a/docs/_static/doctools.js b/docs/_static/doctools.js index e509e48..c3db08d 100644 --- a/docs/_static/doctools.js +++ b/docs/_static/doctools.js @@ -2,325 +2,263 @@ * doctools.js * ~~~~~~~~~~~ * - * Sphinx JavaScript utilities for all documentation. + * Base JavaScript utilities for all Sphinx HTML documentation. * * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. * :license: BSD, see LICENSE for details. * */ +"use strict"; -/** - * select a different prefix for underscore - */ -$u = _.noConflict(); - -/** - * make the code below compatible with browsers without - * an installed firebug like debugger -if (!window.console || !console.firebug) { - var names = ["log", "debug", "info", "warn", "error", "assert", "dir", - "dirxml", "group", "groupEnd", "time", "timeEnd", "count", "trace", - "profile", "profileEnd"]; - window.console = {}; - for (var i = 0; i < names.length; ++i) - window.console[names[i]] = function() {}; -} - */ - -/** - * small helper function to urldecode strings - * - * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL - */ -jQuery.urldecode = function(x) { - if (!x) { - return x +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); } - return decodeURIComponent(x.replace(/\+/g, ' ')); }; /** - * small helper function to urlencode strings + * highlight a given string on a node by wrapping it in + * span elements with the given class name. */ -jQuery.urlencode = encodeURIComponent; +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; -/** - * This function returns the parsed url parameters of the - * current request. Multiple values per key are supported, - * it will always return arrays of strings for the value parts. - */ -jQuery.getQueryParameters = function(s) { - if (typeof s === 'undefined') - s = document.location.search; - var parts = s.substr(s.indexOf('?') + 1).split('&'); - var result = {}; - for (var i = 0; i < parts.length; i++) { - var tmp = parts[i].split('=', 2); - var key = jQuery.urldecode(tmp[0]); - var value = jQuery.urldecode(tmp[1]); - if (key in result) - result[key].push(value); - else - result[key] = [value]; - } - return result; -}; + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } -/** - * highlight a given string on a jquery object by wrapping it in - * span elements with the given class name. - */ -jQuery.fn.highlightText = function(text, className) { - function highlight(node, addItems) { - if (node.nodeType === 3) { - var val = node.nodeValue; - var pos = val.toLowerCase().indexOf(text); - if (pos >= 0 && - !jQuery(node.parentNode).hasClass(className) && - !jQuery(node.parentNode).hasClass("nohighlight")) { - var span; - var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); - if (isInSVG) { - span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); - } else { - span = document.createElement("span"); - span.className = className; - } - span.appendChild(document.createTextNode(val.substr(pos, text.length))); - node.parentNode.insertBefore(span, node.parentNode.insertBefore( + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + parent.insertBefore( + span, + parent.insertBefore( document.createTextNode(val.substr(pos + text.length)), - node.nextSibling)); - node.nodeValue = val.substr(0, pos); - if (isInSVG) { - var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - var bbox = node.parentElement.getBBox(); - rect.x.baseVal.value = bbox.x; - rect.y.baseVal.value = bbox.y; - rect.width.baseVal.value = bbox.width; - rect.height.baseVal.value = bbox.height; - rect.setAttribute('class', className); - addItems.push({ - "parent": node.parentNode, - "target": rect}); - } + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); } } - else if (!jQuery(node).is("button, select, textarea")) { - jQuery.each(node.childNodes, function() { - highlight(this, addItems); - }); - } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); } - var addItems = []; - var result = this.each(function() { - highlight(this, addItems); - }); - for (var i = 0; i < addItems.length; ++i) { - jQuery(addItems[i].parent).before(addItems[i].target); - } - return result; }; - -/* - * backward compatibility for jQuery.browser - * This will be supported until firefox bug is fixed. - */ -if (!jQuery.browser) { - jQuery.uaMatch = function(ua) { - ua = ua.toLowerCase(); - - var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || - /(webkit)[ \/]([\w.]+)/.exec(ua) || - /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || - /(msie) ([\w.]+)/.exec(ua) || - ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || - []; - - return { - browser: match[ 1 ] || "", - version: match[ 2 ] || "0" - }; - }; - jQuery.browser = {}; - jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; -} +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; /** * Small JavaScript module for the documentation. */ -var Documentation = { - - init : function() { - this.fixFirefoxAnchorBug(); - this.highlightSearchWords(); - this.initIndexTable(); - if (DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) { - this.initOnKeyListeners(); - } +const Documentation = { + init: () => { + Documentation.highlightSearchWords(); + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); }, /** * i18n support */ - TRANSLATIONS : {}, - PLURAL_EXPR : function(n) { return n === 1 ? 0 : 1; }, - LOCALE : 'unknown', + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", // gettext and ngettext don't access this so that the functions // can safely bound to a different name (_ = Documentation.gettext) - gettext : function(string) { - var translated = Documentation.TRANSLATIONS[string]; - if (typeof translated === 'undefined') - return string; - return (typeof translated === 'string') ? translated : translated[0]; - }, - - ngettext : function(singular, plural, n) { - var translated = Documentation.TRANSLATIONS[singular]; - if (typeof translated === 'undefined') - return (n == 1) ? singular : plural; - return translated[Documentation.PLURALEXPR(n)]; - }, - - addTranslations : function(catalog) { - for (var key in catalog.messages) - this.TRANSLATIONS[key] = catalog.messages[key]; - this.PLURAL_EXPR = new Function('n', 'return +(' + catalog.plural_expr + ')'); - this.LOCALE = catalog.locale; + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } }, - /** - * add context elements like header anchor links - */ - addContextElements : function() { - $('div[id] > :header:first').each(function() { - $('\u00B6'). - attr('href', '#' + this.id). - attr('title', _('Permalink to this headline')). - appendTo(this); - }); - $('dt[id]').each(function() { - $('\u00B6'). - attr('href', '#' + this.id). - attr('title', _('Permalink to this definition')). - appendTo(this); - }); + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; }, - /** - * workaround a firefox stupidity - * see: https://bugzilla.mozilla.org/show_bug.cgi?id=645075 - */ - fixFirefoxAnchorBug : function() { - if (document.location.hash && $.browser.mozilla) - window.setTimeout(function() { - document.location.href += ''; - }, 10); + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; }, /** * highlight the search words provided in the url in the text */ - highlightSearchWords : function() { - var params = $.getQueryParameters(); - var terms = (params.highlight) ? params.highlight[0].split(/\s+/) : []; - if (terms.length) { - var body = $('div.body'); - if (!body.length) { - body = $('body'); - } - window.setTimeout(function() { - $.each(terms, function() { - body.highlightText(this.toLowerCase(), 'highlighted'); - }); - }, 10); - $('') - .appendTo($('#searchbox')); - } - }, + highlightSearchWords: () => { + const highlight = + new URLSearchParams(window.location.search).get("highlight") || ""; + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do - /** - * init the domain index toggle buttons - */ - initIndexTable : function() { - var togglers = $('img.toggler').click(function() { - var src = $(this).attr('src'); - var idnum = $(this).attr('id').substr(7); - $('tr.cg-' + idnum).toggle(); - if (src.substr(-9) === 'minus.png') - $(this).attr('src', src.substr(0, src.length-9) + 'plus.png'); - else - $(this).attr('src', src.substr(0, src.length-8) + 'minus.png'); - }).css('display', ''); - if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) { - togglers.click(); - } + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); }, /** * helper function to hide the search marks again */ - hideSearchWords : function() { - $('#searchbox .highlight-link').fadeOut(300); - $('span.highlighted').removeClass('highlighted'); - var url = new URL(window.location); - url.searchParams.delete('highlight'); - window.history.replaceState({}, '', url); + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + const url = new URL(window.location); + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); }, /** - * make the url absolute + * helper function to focus on search bar */ - makeURL : function(relativeURL) { - return DOCUMENTATION_OPTIONS.URL_ROOT + '/' + relativeURL; + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); }, /** - * get the current relative url + * Initialise the domain index toggle buttons */ - getCurrentURL : function() { - var path = document.location.pathname; - var parts = path.split(/\//); - $.each(DOCUMENTATION_OPTIONS.URL_ROOT.split(/\//), function() { - if (this === '..') - parts.pop(); - }); - var url = parts.join('/'); - return path.substring(url.lastIndexOf('/') + 1, path.length - 1); + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); }, - initOnKeyListeners: function() { - $(document).keydown(function(event) { - var activeElementType = document.activeElement.tagName; - // don't navigate when in search box, textarea, dropdown or button - if (activeElementType !== 'TEXTAREA' && activeElementType !== 'INPUT' && activeElementType !== 'SELECT' - && activeElementType !== 'BUTTON' && !event.altKey && !event.ctrlKey && !event.metaKey - && !event.shiftKey) { - switch (event.keyCode) { - case 37: // left - var prevHref = $('link[rel="prev"]').prop('href'); - if (prevHref) { - window.location.href = prevHref; - return false; + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + const blacklistedElements = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", + ]); + document.addEventListener("keydown", (event) => { + if (blacklistedElements.has(document.activeElement.tagName)) return; // bail for input elements + if (event.altKey || event.ctrlKey || event.metaKey) return; // bail with special keys + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); } break; - case 39: // right - var nextHref = $('link[rel="next"]').prop('href'); - if (nextHref) { - window.location.href = nextHref; - return false; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); } break; + case "Escape": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.hideSearchWords(); + event.preventDefault(); } } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } }); - } + }, }; // quick alias for translations -_ = Documentation.gettext; +const _ = Documentation.gettext; -$(document).ready(function() { - Documentation.init(); -}); +_ready(Documentation.init); diff --git a/docs/_static/documentation_options.js b/docs/_static/documentation_options.js index 2fa8c97..a750e4d 100644 --- a/docs/_static/documentation_options.js +++ b/docs/_static/documentation_options.js @@ -1,12 +1,14 @@ var DOCUMENTATION_OPTIONS = { URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), VERSION: '', - LANGUAGE: 'None', + LANGUAGE: 'en', COLLAPSE_INDEX: false, BUILDER: 'html', FILE_SUFFIX: '.html', LINK_SUFFIX: '.html', HAS_SOURCE: true, SOURCELINK_SUFFIX: '.txt', - NAVIGATION_WITH_KEYS: false + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: false, }; \ No newline at end of file diff --git a/docs/_static/jquery-3.5.1.js b/docs/_static/jquery-3.6.0.js similarity index 98% rename from docs/_static/jquery-3.5.1.js rename to docs/_static/jquery-3.6.0.js index 5093733..fc6c299 100644 --- a/docs/_static/jquery-3.5.1.js +++ b/docs/_static/jquery-3.6.0.js @@ -1,15 +1,15 @@ /*! - * jQuery JavaScript Library v3.5.1 + * jQuery JavaScript Library v3.6.0 * https://jquery.com/ * * Includes Sizzle.js * https://sizzlejs.com/ * - * Copyright JS Foundation and other contributors + * Copyright OpenJS Foundation and other contributors * Released under the MIT license * https://jquery.org/license * - * Date: 2020-05-04T22:49Z + * Date: 2021-03-02T17:08Z */ ( function( global, factory ) { @@ -76,12 +76,16 @@ var support = {}; var isFunction = function isFunction( obj ) { - // Support: Chrome <=57, Firefox <=52 - // In some browsers, typeof returns "function" for HTML elements - // (i.e., `typeof document.createElement( "object" ) === "function"`). - // We don't want to classify *any* DOM node as a function. - return typeof obj === "function" && typeof obj.nodeType !== "number"; - }; + // Support: Chrome <=57, Firefox <=52 + // In some browsers, typeof returns "function" for HTML elements + // (i.e., `typeof document.createElement( "object" ) === "function"`). + // We don't want to classify *any* DOM node as a function. + // Support: QtWeb <=3.8.5, WebKit <=534.34, wkhtmltopdf tool <=0.12.5 + // Plus for old WebKit, typeof returns "function" for HTML collections + // (e.g., `typeof document.getElementsByTagName("div") === "function"`). (gh-4756) + return typeof obj === "function" && typeof obj.nodeType !== "number" && + typeof obj.item !== "function"; + }; var isWindow = function isWindow( obj ) { @@ -147,7 +151,7 @@ function toType( obj ) { var - version = "3.5.1", + version = "3.6.0", // Define a local copy of jQuery jQuery = function( selector, context ) { @@ -401,7 +405,7 @@ jQuery.extend( { if ( isArrayLike( Object( arr ) ) ) { jQuery.merge( ret, typeof arr === "string" ? - [ arr ] : arr + [ arr ] : arr ); } else { push.call( ret, arr ); @@ -496,9 +500,9 @@ if ( typeof Symbol === "function" ) { // Populate the class2type map jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), -function( _i, name ) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); -} ); + function( _i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); + } ); function isArrayLike( obj ) { @@ -518,14 +522,14 @@ function isArrayLike( obj ) { } var Sizzle = /*! - * Sizzle CSS Selector Engine v2.3.5 + * Sizzle CSS Selector Engine v2.3.6 * https://sizzlejs.com/ * * Copyright JS Foundation and other contributors * Released under the MIT license * https://js.foundation/ * - * Date: 2020-03-14 + * Date: 2021-02-16 */ ( function( window ) { var i, @@ -1108,8 +1112,8 @@ support = Sizzle.support = {}; * @returns {Boolean} True iff elem is a non-HTML XML node */ isXML = Sizzle.isXML = function( elem ) { - var namespace = elem.namespaceURI, - docElem = ( elem.ownerDocument || elem ).documentElement; + var namespace = elem && elem.namespaceURI, + docElem = elem && ( elem.ownerDocument || elem ).documentElement; // Support: IE <=8 // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes @@ -3024,9 +3028,9 @@ var rneedsContext = jQuery.expr.match.needsContext; function nodeName( elem, name ) { - return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); -}; +} var rsingleTag = ( /^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i ); @@ -3997,8 +4001,8 @@ jQuery.extend( { resolveContexts = Array( i ), resolveValues = slice.call( arguments ), - // the master Deferred - master = jQuery.Deferred(), + // the primary Deferred + primary = jQuery.Deferred(), // subordinate callback factory updateFunc = function( i ) { @@ -4006,30 +4010,30 @@ jQuery.extend( { resolveContexts[ i ] = this; resolveValues[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; if ( !( --remaining ) ) { - master.resolveWith( resolveContexts, resolveValues ); + primary.resolveWith( resolveContexts, resolveValues ); } }; }; // Single- and empty arguments are adopted like Promise.resolve if ( remaining <= 1 ) { - adoptValue( singleValue, master.done( updateFunc( i ) ).resolve, master.reject, + adoptValue( singleValue, primary.done( updateFunc( i ) ).resolve, primary.reject, !remaining ); // Use .then() to unwrap secondary thenables (cf. gh-3000) - if ( master.state() === "pending" || + if ( primary.state() === "pending" || isFunction( resolveValues[ i ] && resolveValues[ i ].then ) ) { - return master.then(); + return primary.then(); } } // Multiple arguments are aggregated like Promise.all array elements while ( i-- ) { - adoptValue( resolveValues[ i ], updateFunc( i ), master.reject ); + adoptValue( resolveValues[ i ], updateFunc( i ), primary.reject ); } - return master.promise(); + return primary.promise(); } } ); @@ -4180,8 +4184,8 @@ var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { for ( ; i < len; i++ ) { fn( elems[ i ], key, raw ? - value : - value.call( elems[ i ], i, fn( elems[ i ], key ) ) + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) ); } } @@ -5089,10 +5093,7 @@ function buildFragment( elems, context, scripts, selection, ignored ) { } -var - rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, - rtypenamespace = /^([^.]*)(?:\.(.+)|)/; +var rtypenamespace = /^([^.]*)(?:\.(.+)|)/; function returnTrue() { return true; @@ -5387,8 +5388,8 @@ jQuery.event = { event = jQuery.event.fix( nativeEvent ), handlers = ( - dataPriv.get( this, "events" ) || Object.create( null ) - )[ event.type ] || [], + dataPriv.get( this, "events" ) || Object.create( null ) + )[ event.type ] || [], special = jQuery.event.special[ event.type ] || {}; // Use the fix-ed jQuery.Event rather than the (read-only) native event @@ -5512,12 +5513,12 @@ jQuery.event = { get: isFunction( hook ) ? function() { if ( this.originalEvent ) { - return hook( this.originalEvent ); + return hook( this.originalEvent ); } } : function() { if ( this.originalEvent ) { - return this.originalEvent[ name ]; + return this.originalEvent[ name ]; } }, @@ -5656,7 +5657,13 @@ function leverageNative( el, type, expectSync ) { // Cancel the outer synthetic event event.stopImmediatePropagation(); event.preventDefault(); - return result.value; + + // Support: Chrome 86+ + // In Chrome, if an element having a focusout handler is blurred by + // clicking outside of it, it invokes the handler synchronously. If + // that handler calls `.remove()` on the element, the data is cleared, + // leaving `result` undefined. We need to guard against this. + return result && result.value; } // If this is an inner synthetic event for an event with a bubbling surrogate @@ -5821,34 +5828,7 @@ jQuery.each( { targetTouches: true, toElement: true, touches: true, - - which: function( event ) { - var button = event.button; - - // Add which for key events - if ( event.which == null && rkeyEvent.test( event.type ) ) { - return event.charCode != null ? event.charCode : event.keyCode; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - if ( !event.which && button !== undefined && rmouseEvent.test( event.type ) ) { - if ( button & 1 ) { - return 1; - } - - if ( button & 2 ) { - return 3; - } - - if ( button & 4 ) { - return 2; - } - - return 0; - } - - return event.which; - } + which: true }, jQuery.event.addProp ); jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateType ) { @@ -5874,6 +5854,12 @@ jQuery.each( { focus: "focusin", blur: "focusout" }, function( type, delegateTyp return true; }, + // Suppress native focus or blur as it's already being fired + // in leverageNative. + _default: function() { + return true; + }, + delegateType: delegateType }; } ); @@ -6541,6 +6527,10 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); // set in CSS while `offset*` properties report correct values. // Behavior in IE 9 is more subtle than in newer versions & it passes // some versions of this test; make sure not to make it pass there! + // + // Support: Firefox 70+ + // Only Firefox includes border widths + // in computed dimensions. (gh-4529) reliableTrDimensions: function() { var table, tr, trChild, trStyle; if ( reliableTrDimensionsVal == null ) { @@ -6548,17 +6538,32 @@ var rboxStyle = new RegExp( cssExpand.join( "|" ), "i" ); tr = document.createElement( "tr" ); trChild = document.createElement( "div" ); - table.style.cssText = "position:absolute;left:-11111px"; + table.style.cssText = "position:absolute;left:-11111px;border-collapse:separate"; + tr.style.cssText = "border:1px solid"; + + // Support: Chrome 86+ + // Height set through cssText does not get applied. + // Computed height then comes back as 0. tr.style.height = "1px"; trChild.style.height = "9px"; + // Support: Android 8 Chrome 86+ + // In our bodyBackground.html iframe, + // display for all div elements is set to "inline", + // which causes a problem only in Android 8 Chrome 86. + // Ensuring the div is display: block + // gets around this issue. + trChild.style.display = "block"; + documentElement .appendChild( table ) .appendChild( tr ) .appendChild( trChild ); trStyle = window.getComputedStyle( tr ); - reliableTrDimensionsVal = parseInt( trStyle.height ) > 3; + reliableTrDimensionsVal = ( parseInt( trStyle.height, 10 ) + + parseInt( trStyle.borderTopWidth, 10 ) + + parseInt( trStyle.borderBottomWidth, 10 ) ) === tr.offsetHeight; documentElement.removeChild( table ); } @@ -7022,10 +7027,10 @@ jQuery.each( [ "height", "width" ], function( _i, dimension ) { // Running getBoundingClientRect on a disconnected node // in IE throws an error. ( !elem.getClientRects().length || !elem.getBoundingClientRect().width ) ? - swap( elem, cssShow, function() { - return getWidthOrHeight( elem, dimension, extra ); - } ) : - getWidthOrHeight( elem, dimension, extra ); + swap( elem, cssShow, function() { + return getWidthOrHeight( elem, dimension, extra ); + } ) : + getWidthOrHeight( elem, dimension, extra ); } }, @@ -7084,7 +7089,7 @@ jQuery.cssHooks.marginLeft = addGetHookIf( support.reliableMarginLeft, swap( elem, { marginLeft: 0 }, function() { return elem.getBoundingClientRect().left; } ) - ) + "px"; + ) + "px"; } } ); @@ -7223,7 +7228,7 @@ Tween.propHooks = { if ( jQuery.fx.step[ tween.prop ] ) { jQuery.fx.step[ tween.prop ]( tween ); } else if ( tween.elem.nodeType === 1 && ( - jQuery.cssHooks[ tween.prop ] || + jQuery.cssHooks[ tween.prop ] || tween.elem.style[ finalPropName( tween.prop ) ] != null ) ) { jQuery.style( tween.elem, tween.prop, tween.now + tween.unit ); } else { @@ -7468,7 +7473,7 @@ function defaultPrefilter( elem, props, opts ) { anim.done( function() { - /* eslint-enable no-loop-func */ + /* eslint-enable no-loop-func */ // The final step of a "hide" animation is actually hiding the element if ( !hidden ) { @@ -7588,7 +7593,7 @@ function Animation( elem, properties, options ) { tweens: [], createTween: function( prop, end ) { var tween = jQuery.Tween( elem, animation.opts, prop, end, - animation.opts.specialEasing[ prop ] || animation.opts.easing ); + animation.opts.specialEasing[ prop ] || animation.opts.easing ); animation.tweens.push( tween ); return tween; }, @@ -7761,7 +7766,8 @@ jQuery.fn.extend( { anim.stop( true ); } }; - doAnimation.finish = doAnimation; + + doAnimation.finish = doAnimation; return empty || optall.queue === false ? this.each( doAnimation ) : @@ -8401,8 +8407,8 @@ jQuery.fn.extend( { if ( this.setAttribute ) { this.setAttribute( "class", className || value === false ? - "" : - dataPriv.get( this, "__className__" ) || "" + "" : + dataPriv.get( this, "__className__" ) || "" ); } } @@ -8417,7 +8423,7 @@ jQuery.fn.extend( { while ( ( elem = this[ i++ ] ) ) { if ( elem.nodeType === 1 && ( " " + stripAndCollapse( getClass( elem ) ) + " " ).indexOf( className ) > -1 ) { - return true; + return true; } } @@ -8707,9 +8713,7 @@ jQuery.extend( jQuery.event, { special.bindType || type; // jQuery handler - handle = ( - dataPriv.get( cur, "events" ) || Object.create( null ) - )[ event.type ] && + handle = ( dataPriv.get( cur, "events" ) || Object.create( null ) )[ event.type ] && dataPriv.get( cur, "handle" ); if ( handle ) { handle.apply( cur, data ); @@ -8856,7 +8860,7 @@ var rquery = ( /\?/ ); // Cross-browser xml parsing jQuery.parseXML = function( data ) { - var xml; + var xml, parserErrorElem; if ( !data || typeof data !== "string" ) { return null; } @@ -8865,12 +8869,17 @@ jQuery.parseXML = function( data ) { // IE throws on parseFromString with invalid input. try { xml = ( new window.DOMParser() ).parseFromString( data, "text/xml" ); - } catch ( e ) { - xml = undefined; - } + } catch ( e ) {} - if ( !xml || xml.getElementsByTagName( "parsererror" ).length ) { - jQuery.error( "Invalid XML: " + data ); + parserErrorElem = xml && xml.getElementsByTagName( "parsererror" )[ 0 ]; + if ( !xml || parserErrorElem ) { + jQuery.error( "Invalid XML: " + ( + parserErrorElem ? + jQuery.map( parserErrorElem.childNodes, function( el ) { + return el.textContent; + } ).join( "\n" ) : + data + ) ); } return xml; }; @@ -8971,16 +8980,14 @@ jQuery.fn.extend( { // Can add propHook for "elements" to filter or add form elements var elements = jQuery.prop( this, "elements" ); return elements ? jQuery.makeArray( elements ) : this; - } ) - .filter( function() { + } ).filter( function() { var type = this.type; // Use .is( ":disabled" ) so that fieldset[disabled] works return this.name && !jQuery( this ).is( ":disabled" ) && rsubmittable.test( this.nodeName ) && !rsubmitterTypes.test( type ) && ( this.checked || !rcheckableType.test( type ) ); - } ) - .map( function( _i, elem ) { + } ).map( function( _i, elem ) { var val = jQuery( this ).val(); if ( val == null ) { @@ -9033,7 +9040,8 @@ var // Anchor tag for parsing the document origin originAnchor = document.createElement( "a" ); - originAnchor.href = location.href; + +originAnchor.href = location.href; // Base "constructor" for jQuery.ajaxPrefilter and jQuery.ajaxTransport function addToPrefiltersOrTransports( structure ) { @@ -9414,8 +9422,8 @@ jQuery.extend( { // Context for global events is callbackContext if it is a DOM node or jQuery collection globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ? - jQuery( callbackContext ) : - jQuery.event, + jQuery( callbackContext ) : + jQuery.event, // Deferreds deferred = jQuery.Deferred(), @@ -9727,8 +9735,10 @@ jQuery.extend( { response = ajaxHandleResponses( s, jqXHR, responses ); } - // Use a noop converter for missing script - if ( !isSuccess && jQuery.inArray( "script", s.dataTypes ) > -1 ) { + // Use a noop converter for missing script but not if jsonp + if ( !isSuccess && + jQuery.inArray( "script", s.dataTypes ) > -1 && + jQuery.inArray( "json", s.dataTypes ) < 0 ) { s.converters[ "text script" ] = function() {}; } @@ -10466,12 +10476,6 @@ jQuery.offset = { options.using.call( elem, props ); } else { - if ( typeof props.top === "number" ) { - props.top += "px"; - } - if ( typeof props.left === "number" ) { - props.left += "px"; - } curElem.css( props ); } } @@ -10640,8 +10644,11 @@ jQuery.each( [ "top", "left" ], function( _i, prop ) { // Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods jQuery.each( { Height: "height", Width: "width" }, function( name, type ) { - jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, - function( defaultExtra, funcName ) { + jQuery.each( { + padding: "inner" + name, + content: type, + "": "outer" + name + }, function( defaultExtra, funcName ) { // Margin is only for outerHeight, outerWidth jQuery.fn[ funcName ] = function( margin, value ) { @@ -10726,7 +10733,8 @@ jQuery.fn.extend( { } } ); -jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " + +jQuery.each( + ( "blur focus focusin focusout resize scroll click dblclick " + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + "change select submit keydown keypress keyup contextmenu" ).split( " " ), function( _i, name ) { @@ -10737,7 +10745,8 @@ jQuery.each( ( "blur focus focusin focusout resize scroll click dblclick " + this.on( name, null, data, fn ) : this.trigger( name ); }; - } ); + } +); diff --git a/docs/_static/jquery.js b/docs/_static/jquery.js index b061403..c4c6022 100644 --- a/docs/_static/jquery.js +++ b/docs/_static/jquery.js @@ -1,2 +1,2 @@ -/*! jQuery v3.5.1 | (c) JS Foundation and other contributors | jquery.org/license */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.5.1",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e.namespaceURI,n=(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},D=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function D(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||j,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,j=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function qe(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function Le(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function He(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Oe(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):("number"==typeof f.top&&(f.top+="px"),"number"==typeof f.left&&(f.left+="px"),c.css(f))}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=$e(y.pixelPosition,function(e,t){if(t)return t=Be(e,n),Me.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 { + const [docname, title, anchor, descr, score, filename] = result + return score }, */ @@ -28,9 +30,11 @@ if (!Scorer) { // or matches in the last dotted part of the object name objPartialMatch: 6, // Additive scores depending on the priority of the object - objPrio: {0: 15, // used to be importantResults - 1: 5, // used to be objectResults - 2: -5}, // used to be unimportantResults + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, // Used when the priority is not in the mapping. objPrioDefault: 0, @@ -39,456 +43,455 @@ if (!Scorer) { partialTitle: 7, // query found in terms term: 5, - partialTerm: 2 + partialTerm: 2, }; } -if (!splitQuery) { - function splitQuery(query) { - return query.split(/\s+/); +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, highlightTerms, searchTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docUrlRoot = DOCUMENTATION_OPTIONS.URL_ROOT; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + + const [docName, title, anchor, descr] = item; + + let listItem = document.createElement("li"); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = docUrlRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = docUrlRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + const params = new URLSearchParams(); + params.set("highlight", [...highlightTerms].join(" ")); + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + "?" + params.toString() + anchor; + linkEl.innerHTML = title; + if (descr) + listItem.appendChild(document.createElement("span")).innerText = + " (" + descr + ")"; + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, highlightTerms) + ); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = _( + `Search finished, found ${resultCount} page(s) matching the search query.` + ); +}; +const _displayNextItem = ( + results, + resultCount, + highlightTerms, + searchTerms +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), highlightTerms, searchTerms); + setTimeout( + () => _displayNextItem(results, resultCount, highlightTerms, searchTerms), + 5 + ); } + // search finished, update title and status message + else _finishSearch(resultCount); +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings } /** * Search Module */ -var Search = { - - _index : null, - _queued_query : null, - _pulse_status : -1, - - htmlToText : function(htmlString) { - var virtualDocument = document.implementation.createHTMLDocument('virtual'); - var htmlElement = $(htmlString, virtualDocument); - htmlElement.find('.headerlink').remove(); - docContent = htmlElement.find('[role=main]')[0]; - if(docContent === undefined) { - console.warn("Content block not found. Sphinx search tries to obtain it " + - "via '[role=main]'. Could you check your theme or template."); - return ""; - } - return docContent.textContent || docContent.innerText; +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString) => { + const htmlElement = document + .createRange() + .createContextualFragment(htmlString); + _removeChildren(htmlElement.querySelectorAll(".headerlink")); + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent !== undefined) return docContent.textContent; + console.warn( + "Content block not found. Sphinx search tries to obtain it via '[role=main]'. Could you check your theme or template." + ); + return ""; }, - init : function() { - var params = $.getQueryParameters(); - if (params.q) { - var query = params.q[0]; - $('input[name="q"]')[0].value = query; - this.performSearch(query); - } + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); }, - loadIndex : function(url) { - $.ajax({type: "GET", url: url, data: null, - dataType: "script", cache: true, - complete: function(jqxhr, textstatus) { - if (textstatus != "success") { - document.getElementById("searchindexloader").src = url; - } - }}); - }, + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), - setIndex : function(index) { - var q; - this._index = index; - if ((q = this._queued_query) !== null) { - this._queued_query = null; - Search.query(q); + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); } }, - hasIndex : function() { - return this._index !== null; - }, + hasIndex: () => Search._index !== null, - deferQuery : function(query) { - this._queued_query = query; - }, + deferQuery: (query) => (Search._queued_query = query), - stopPulse : function() { - this._pulse_status = 0; - }, + stopPulse: () => (Search._pulse_status = -1), - startPulse : function() { - if (this._pulse_status >= 0) - return; - function pulse() { - var i; + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { Search._pulse_status = (Search._pulse_status + 1) % 4; - var dotString = ''; - for (i = 0; i < Search._pulse_status; i++) - dotString += '.'; - Search.dots.text(dotString); - if (Search._pulse_status > -1) - window.setTimeout(pulse, 500); - } + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; pulse(); }, /** * perform a search for something (or wait until index is loaded) */ - performSearch : function(query) { + performSearch: (query) => { // create the required interface elements - this.out = $('#search-results'); - this.title = $('

' + _('Searching') + '

').appendTo(this.out); - this.dots = $('').appendTo(this.title); - this.status = $('

 

').appendTo(this.out); - this.output = $('