Skip to content

Commit

Permalink
General Changes
Browse files Browse the repository at this point in the history
- Add `RoomIdAPIRoute`
- Rename `RoomIdRoute` to `RoomIdHTMLRoute`
- Add fallback room ID method
- Disable room info by default
- Add `fetch_live_check` start param instead of room info (due to 18+ restriction issue)
- Refactor & clean some code
  • Loading branch information
isaackogan committed Apr 8, 2024
1 parent 852943a commit eb243c7
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 48 deletions.
27 changes: 16 additions & 11 deletions TikTokLive/client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from pyee.base import Handler

from TikTokLive.client.errors import AlreadyConnectedError, UserOfflineError, InitialCursorMissingError, \
WebsocketURLMissingError, AgeRestrictedError
WebsocketURLMissingError
from TikTokLive.client.logger import TikTokLiveLogHandler, LogLevel
from TikTokLive.client.web.web_client import TikTokWebClient
from TikTokLive.client.web.web_settings import WebDefaults
Expand Down Expand Up @@ -92,8 +92,9 @@ async def start(
self,
*,
process_connect_events: bool = True,
fetch_room_info: bool = True,
fetch_room_info: bool = False,
fetch_gift_info: bool = False,
fetch_live_check: bool = True,
room_id: Optional[int] = None
) -> Task:
"""
Expand All @@ -102,6 +103,7 @@ async def start(
:param process_connect_events: Whether to process initial events sent on room join
:param fetch_room_info: Whether to fetch room info on join
:param fetch_gift_info: Whether to fetch gift info on join
:param fetch_live_check: Whether to check if the user is live (you almost ALWAYS want this enabled)
:param room_id: An override to the room ID to connect directly to the livestream and skip scraping the live.
Useful when trying to scale, as scraping the HTML can result in TikTok blocks.
:return: Task containing the heartbeat of the client
Expand All @@ -112,19 +114,22 @@ async def start(
raise AlreadyConnectedError("You can only make one connection per client!")

# <Required> Fetch room ID
self._room_id: str = room_id or await self._web.fetch_room_id(self._unique_id)
try:
self._room_id: str = room_id or await self._web.fetch_room_id_from_html(self._unique_id)
except Exception as base_ex:
try:
self._logger.error("Failed to parse room ID from HTML. Using API fallback.")
self._room_id: str = await self._web.fetch_room_id_from_api(self.unique_id)
except Exception as super_ex:
raise super_ex from base_ex

# <Optional> Fetch live status
if fetch_live_check and not await self._web.fetch_is_live(room_id=self._room_id):
raise UserOfflineError()

# <Optional> Fetch room info
if fetch_room_info:
self._room_info = await self._web.fetch_room_info()
if "prompts" in self._room_info and len(self._room_info) == 1:
raise AgeRestrictedError(
"Age restricted stream. "
"Pass sessionid to log in & bypass age restriction, OR set fetch_room_info=False "
"in the client connection method."
)
if self._room_info.get("status", 4) == 4:
raise UserOfflineError()

# <Optional> Fetch gift info
if fetch_gift_info:
Expand Down
2 changes: 1 addition & 1 deletion TikTokLive/client/web/routes/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from .fetch_gift_list import GiftListRoute
from .fetch_image import ImageFetchRoute
from .fetch_room_id import RoomIdRoute
from .fetch_room_id_html import RoomIdHTMLRoute
from .fetch_room_info import FetchRoomInfoRoute
from .fetch_sign import SignFetchRoute
from .fetch_video import VideoFetchRoute
Expand Down
36 changes: 4 additions & 32 deletions TikTokLive/client/web/routes/fetch_is_live.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from httpx import Response

from TikTokLive.client.web.routes.fetch_room_id_api import RoomIdAPIRoute
from TikTokLive.client.web.web_base import ClientRoute
from TikTokLive.client.web.web_settings import WebDefaults

Expand All @@ -15,18 +16,6 @@ class InvalidFetchIsLiveRequest(RuntimeError):
pass


class InvalidLiveUser(RuntimeError):
"""
Thrown when the request to check if a user is live fails because a user has no
livestream account (e.g. <1000 followers)
"""

def __init__(self, unique_id: str, *args):
self.unique_id: str = unique_id
super().__init__(*args)


class FetchIsLiveRoute(ClientRoute):
"""
Check if a given user is alive through their unique_id or room_id
Expand Down Expand Up @@ -87,26 +76,9 @@ async def fetch_is_live_unique_id(self, unique_id: str) -> bool:
"""

response: Response = await self._web.get_response(
url=WebDefaults.tiktok_app_url + f"/api-live/user/room/",
extra_params=(
{
"uniqueId": unique_id,
"sourceType": 54
}
)
response_json: dict = await RoomIdAPIRoute.fetch_user_room_data(
web=self._web,
unique_id=unique_id
)

response_json: dict = response.json()

# Invalid user
if response_json["message"] == "user_not_found":
raise InvalidLiveUser(
unique_id,
(
f"The requested user '{unique_id}' is not capable of going LIVE on TikTok, "
"or has never gone live on TikTok, or does not exist."
)
)

return response_json["data"]["liveRoom"]["status"] != 4
95 changes: 95 additions & 0 deletions TikTokLive/client/web/routes/fetch_room_id_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
from httpx import Response

from TikTokLive.client.web.routes.fetch_room_id_html import FailedParseRoomIdError
from TikTokLive.client.web.web_base import ClientRoute, TikTokHTTPClient
from TikTokLive.client.web.web_settings import WebDefaults


class UserNotFound(RuntimeError):
"""
Thrown when the request to check if a user is live fails because a user has no
livestream account (e.g. <1000 followers)
"""

def __init__(self, unique_id: str, *args):
self.unique_id: str = unique_id
super().__init__(*args)


class RoomIdAPIRoute(ClientRoute):
"""
Route to retrieve the room ID for a user
"""

async def __call__(self, unique_id: str) -> str:
"""
Fetch the Room ID for a given unique_id from the TikTok API
:param unique_id: The user's uniqueId
:return: The room ID string
"""

# Get their livestream room ID from the api
room_data: dict = await self.fetch_user_room_data(
web=self._web,
unique_id=unique_id
)

# Parse & update the web client
room_id = self._web.params["room_id"] = self.parse_room_id(room_data)
return room_id

@classmethod
async def fetch_user_room_data(cls, web: TikTokHTTPClient, unique_id: str) -> dict:
"""
Fetch user room from the API (not the same as room info)
:param web: The TikTokHTTPClient client to use
:param unique_id: The user to check
:return: The user's room info
"""

response: Response = await web.get_response(
url=WebDefaults.tiktok_app_url + f"/api-live/user/room/",
extra_params=(
{
"uniqueId": unique_id,
"sourceType": 54
}
)
)

response_json: dict = response.json()

# Invalid user
if response_json["message"] == "user_not_found":
raise UserNotFound(
unique_id,
(
f"The requested user '{unique_id}' is not capable of going LIVE on TikTok, "
"or has never gone live on TikTok, or does not exist."
)
)

return response_json

@classmethod
def parse_room_id(cls, data: dict) -> str:
"""
Parse the room ID from livestream API response
:param data: The data to parse
:return: The user's room id
:raises: UserOfflineError if the user is offline
:raises: FailedParseRoomIdError if the user data does not exist
"""

try:
return data['data']['user']['roomId']
except KeyError:
raise FailedParseRoomIdError("That user can't stream, or you might be blocked by TikTok.")
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class FailedParseRoomIdError(RuntimeError):
"""


class RoomIdRoute(ClientRoute):
class RoomIdHTMLRoute(ClientRoute):
"""
Route to retrieve the room ID for a user
Expand Down
14 changes: 13 additions & 1 deletion TikTokLive/client/web/routes/fetch_room_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from httpx import Response

from TikTokLive.client.errors import AgeRestrictedError
from TikTokLive.client.web.web_base import ClientRoute
from TikTokLive.client.web.web_settings import WebDefaults

Expand Down Expand Up @@ -30,12 +31,23 @@ async def __call__(self, room_id: Optional[str] = None) -> Dict[str, Any]:

try:

# Fetch from API
response: Response = await self._web.get_response(
url=WebDefaults.tiktok_webcast_url + "/room/info/",
extra_params={"room_id": room_id or self._web.params["room_id"]}
)

return response.json()["data"]
# Get data
data: dict = response.json()["data"]

except Exception as ex:
raise FailedFetchRoomInfoError from ex

# If age restricted
if "prompts" in data and len(data) == 1:
raise AgeRestrictedError(
"Age restricted stream. Cannot fetch room info. "
"Pass sessionid to log in & bypass age restriction."
)

return data
6 changes: 4 additions & 2 deletions TikTokLive/client/web/web_client.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from TikTokLive.client.web.routes import FetchIsLiveRoute
from TikTokLive.client.web.routes.fetch_room_id_api import RoomIdAPIRoute
from TikTokLive.client.web.routes.fetch_video import VideoFetchRoute
from TikTokLive.client.web.routes.fetch_gift_list import GiftListRoute
from TikTokLive.client.web.routes.fetch_image import ImageFetchRoute
from TikTokLive.client.web.routes.fetch_room_id import RoomIdRoute
from TikTokLive.client.web.routes.fetch_room_id_html import RoomIdHTMLRoute
from TikTokLive.client.web.routes.fetch_room_info import FetchRoomInfoRoute
from TikTokLive.client.web.routes.fetch_sign import SignFetchRoute
from TikTokLive.client.web.web_base import TikTokHTTPClient
Expand All @@ -24,7 +25,8 @@ def __init__(self, **kwargs):

super().__init__(**kwargs)

self.fetch_room_id: RoomIdRoute = RoomIdRoute(self)
self.fetch_room_id_from_html: RoomIdHTMLRoute = RoomIdHTMLRoute(self)
self.fetch_room_id_from_api: RoomIdAPIRoute = RoomIdAPIRoute(self)
self.fetch_room_info: FetchRoomInfoRoute = FetchRoomInfoRoute(self)
self.fetch_gift_list: GiftListRoute = GiftListRoute(self)
self.fetch_image: ImageFetchRoute = ImageFetchRoute(self)
Expand Down

0 comments on commit eb243c7

Please sign in to comment.