From e6310be5a2de0ab03440e70ce67b3f5086c97996 Mon Sep 17 00:00:00 2001 From: David Teather <34144122+davidteather@users.noreply.github.com> Date: Mon, 20 Jan 2025 22:46:57 -0600 Subject: [PATCH] V7.0.0 - feat: New Playlist Class, fixes bot detection, update docstring (#1215) * fix: unable to find byted acrawler * fix: playwright timeout sessions with provide opt-out params when creating sessions (#1196) * timeout: 300s -> 30s * fix: tiktok returned invalid reponse by bot detection (#1197) * chore: move wait for load state between mouse moves * Update docstring for create_session() (#1202) * Add playlist class (#1207) * feat: add playlist class * bump version, fix tests --------- Co-authored-by: Rahmat Slamet Co-authored-by: brandon <7378997+anarchopythonista@users.noreply.github.com> Co-authored-by: Lukas --- .sphinx/conf.py | 2 +- CITATION.cff | 4 +- README.md | 17 +- TikTokApi/api/playlist.py | 167 ++++++++++++++++++++ TikTokApi/api/user.py | 58 +++---- TikTokApi/stealth/js/navigator_userAgent.py | 16 +- TikTokApi/tiktok.py | 45 +++++- examples/comment_example.py | 2 +- examples/hashtag_example.py | 2 +- examples/playlist_example.py | 24 +++ examples/search_example.py | 2 +- examples/sound_example.py | 2 +- examples/trending_example.py | 2 +- examples/user_example.py | 2 +- examples/video_example.py | 2 +- imgs/webshare.png | Bin 0 -> 10325 bytes setup.py | 4 +- tests/test_comments.py | 2 +- tests/test_hashtag.py | 8 +- tests/test_integration.py | 2 +- tests/test_playlist.py | 38 +++++ tests/test_search.py | 4 +- tests/test_sound.py | 6 +- tests/test_trending.py | 4 +- tests/test_user.py | 8 +- tests/test_video.py | 8 +- 26 files changed, 358 insertions(+), 73 deletions(-) create mode 100644 TikTokApi/api/playlist.py create mode 100644 examples/playlist_example.py create mode 100644 imgs/webshare.png create mode 100644 tests/test_playlist.py diff --git a/.sphinx/conf.py b/.sphinx/conf.py index 9641c14e..f98655e8 100644 --- a/.sphinx/conf.py +++ b/.sphinx/conf.py @@ -16,7 +16,7 @@ project = "TikTokAPI" copyright = "2023, David Teather" author = "David Teather" -release = "v6.5.2" +release = "v7.0.0" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/main/usage/configuration.html#general-configuration diff --git a/CITATION.cff b/CITATION.cff index 23d02af5..94df0aef 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -5,5 +5,5 @@ authors: orcid: "https://orcid.org/0000-0002-9467-4676" title: "TikTokAPI" url: "https://github.com/davidteather/tiktok-api" -version: 6.5.2 -date-released: 2024-08-24 +version: 7.0.0 +date-released: 2025-01-20 diff --git a/README.md b/README.md index b0810e57..ddd2953f 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ This is an unofficial api wrapper for TikTok.com in python. With this api you ar [![DOI](https://zenodo.org/badge/188710490.svg)](https://zenodo.org/badge/latestdoi/188710490) [![LinkedIn](https://img.shields.io/badge/LinkedIn-0077B5?style=for-the-badge&logo=linkedin&logoColor=white&style=flat-square)](https://www.linkedin.com/in/davidteather/) [![Sponsor Me](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub)](https://github.com/sponsors/davidteather) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/davidteather/TikTok-Api)](https://github.com/davidteather/TikTok-Api/releases) [![GitHub](https://img.shields.io/github/license/davidteather/TikTok-Api)](https://github.com/davidteather/TikTok-Api/blob/main/LICENSE) [![Downloads](https://pepy.tech/badge/tiktokapi)](https://pypi.org/project/TikTokApi/) ![](https://visitor-badge.laobi.icu/badge?page_id=davidteather.TikTok-Api) [![Support Server](https://img.shields.io/discord/783108952111579166.svg?color=7289da&logo=discord&style=flat-square)](https://discord.gg/yyPhbfma6f) -This api is designed to **retrieve data** TikTok. It **can not be used post or upload** content to TikTok on the behalf of a user. It has **no support any user-authenticated routes**, if you can't access it while being logged out on their website you can't access it here. +This api is designed to **retrieve data** TikTok. It **can not be used post or upload** content to TikTok on the behalf of a user. It has **no support for any user-authenticated routes**, if you can't access it while being logged out on their website you can't access it here. ## Sponsors -These sponsors have paid to be placed here and beyond that I do not have any affiliation with them, the TikTokAPI package will always be free and open-source. If you wish to be a sponsor of this project check out my [GitHub sponsors page](https://github.com/sponsors/davidteather). +These sponsors have paid to be placed here or are my own affiliate links which I may earn a commission from, and beyond that I do not have any affiliation with them. The TikTokAPI package will always be free and open-source. If you wish to be a sponsor of this project check out my [GitHub sponsors page](https://github.com/sponsors/davidteather).
@@ -33,6 +33,14 @@ These sponsors have paid to be placed here and beyond that I do not have any aff TikTok Captcha Solver: Bypass any TikTok captcha in just two lines of code.
Scale your TikTok automation and get unblocked with SadCaptcha.
+
+ + TikTok Captcha Solver + +
+ Cheap, Reliable Proxies: Supercharge your web scraping with fast, reliable proxies. Try 10 free datacenter proxies today! +
+
## Table of Contents @@ -93,7 +101,8 @@ docker run -v TikTokApi --rm tiktokapi:latest python3 your_script.py ### Common Issues -Please don't open an issue if you're experiencing one of these just comment if the provided solution do not work for you. +- **EmptyResponseException** - this means TikTok is blocking the request and detects you're a bot. This can be a problem with your setup or the library itself + - you may need a proxy to successfuly scrape TikTok, I've made a [web scraping lesson](https://github.com/davidteather/everything-web-scraping/tree/main/002-proxies) explaining the differences of "tiers" of proxies, I've personally had success with [webshare's residential proxies](https://www.webshare.io/?referral_code=3x5812idzzzp) (affiliate link), but you might have success on their free data center IPs or a cheaper competitor. - **Browser Has no Attribute** - make sure you ran `python3 -m playwright install`, if your error persists try the [playwright-python](https://github.com/microsoft/playwright-python) quickstart guide and diagnose issues from there. @@ -114,7 +123,7 @@ ms_token = os.environ.get("ms_token", None) # get your own ms_token from your co async def trending_videos(): async with TikTokApi() as api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) async for video in api.trending.videos(count=30): print(video) print(video.as_dict) diff --git a/TikTokApi/api/playlist.py b/TikTokApi/api/playlist.py new file mode 100644 index 00000000..cc7a465e --- /dev/null +++ b/TikTokApi/api/playlist.py @@ -0,0 +1,167 @@ +from __future__ import annotations +from typing import TYPE_CHECKING, ClassVar, Iterator, Optional +from ..exceptions import InvalidResponseException + +if TYPE_CHECKING: + from ..tiktok import TikTokApi + from .video import Video + from .user import User + + +class Playlist: + """ + A TikTok video playlist. + + Example Usage: + .. code-block:: python + + playlist = api.playlist(id='7426714779919797038') + """ + + parent: ClassVar[TikTokApi] + + id: Optional[str] + """The ID of the playlist.""" + name: Optional[str] + """The name of the playlist.""" + video_count: Optional[int] + """The video count of the playlist.""" + creator: Optional[User] + """The creator of the playlist.""" + cover_url: Optional[str] + """The cover URL of the playlist.""" + as_dict: dict + """The raw data associated with this Playlist.""" + + def __init__( + self, + id: Optional[str] = None, + data: Optional[dict] = None, + ): + """ + You must provide the playlist id or playlist data otherwise this + will not function correctly. + """ + + if id is None and data.get("id") is None: + raise TypeError("You must provide id parameter.") + + self.id = id + + if data is not None: + self.as_dict = data + self.__extract_from_data() + + async def info(self, **kwargs) -> dict: + """ + Returns a dictionary of information associated with this Playlist. + + Returns: + dict: A dictionary of information associated with this Playlist. + + Raises: + InvalidResponseException: If TikTok returns an invalid response, or one we don't understand. + + Example Usage: + .. code-block:: python + + user_data = await api.playlist(id='7426714779919797038').info() + """ + + id = getattr(self, "id", None) + if not id: + raise TypeError( + "You must provide the playlist id when creating this class to use this method." + ) + + url_params = { + "mixId": id, + "msToken": kwargs.get("ms_token"), + } + + resp = await self.parent.make_request( + url="https://www.tiktok.com/api/mix/detail/", + params=url_params, + headers=kwargs.get("headers"), + session_index=kwargs.get("session_index"), + ) + + if resp is None: + raise InvalidResponseException(resp, "TikTok returned an invalid response.") + + self.as_dict = resp["mixInfo"] + self.__extract_from_data() + return resp + + async def videos(self, count=30, cursor=0, **kwargs) -> Iterator[Video]: + """ + Returns an iterator of videos in this User's playlist. + + Returns: + Iterator[dict]: An iterator of videos in this User's playlist. + + Raises: + InvalidResponseException: If TikTok returns an invalid response, or one we don't understand. + + Example Usage: + .. code-block:: python + + playlist_videos = await api.playlist(id='7426714779919797038').videos() + """ + id = getattr(self, "id", None) + if id is None or id == "": + await self.info(**kwargs) + + found = 0 + while found < count: + params = { + "mixId": id, + "count": min(count, 30), + "cursor": cursor, + } + + resp = await self.parent.make_request( + url="https://www.tiktok.com/api/mix/item_list/", + params=params, + headers=kwargs.get("headers"), + session_index=kwargs.get("session_index"), + ) + + if resp is None: + raise InvalidResponseException( + resp, "TikTok returned an invalid response." + ) + + for video in resp.get("itemList", []): + yield self.parent.video(data=video) + found += 1 + + if not resp.get("hasMore", False): + return + + cursor = resp.get("cursor") + + def __extract_from_data(self): + data = self.as_dict + keys = data.keys() + + if "mixInfo" in keys: + data = data["mixInfo"] + + self.id = data.get("id", None) or data.get("mixId", None) + self.name = data.get("name", None) or data.get("mixName", None) + self.video_count = data.get("videoCount", None) + self.creator = self.parent.user(data=data.get("creator", {})) + self.cover_url = data.get("cover", None) + + if None in [self.id, self.name, self.video_count, self.creator, self.cover_url]: + User.parent.logger.error( + f"Failed to create Playlist with data: {data}\nwhich has keys {data.keys()}" + ) + + def __repr__(self): + return self.__str__() + + def __str__(self): + id = getattr(self, "id", None) + return f"TikTokApi.playlist(id='{id}'')" diff --git a/TikTokApi/api/user.py b/TikTokApi/api/user.py index 5877472b..33da47d9 100644 --- a/TikTokApi/api/user.py +++ b/TikTokApi/api/user.py @@ -5,6 +5,7 @@ if TYPE_CHECKING: from ..tiktok import TikTokApi from .video import Video + from .playlist import Playlist class User: @@ -87,12 +88,12 @@ async def info(self, **kwargs) -> dict: self.__extract_from_data() return resp - async def playlists(self, count=20, cursor=0, **kwargs) -> Iterator[dict]: + async def playlists(self, count=20, cursor=0, **kwargs) -> Iterator[Playlist]: """ - Returns a dictionary of information associated with this User's playlist. + Returns a user's playlists. Returns: - dict: A dictionary of information associated with this User's playlist. + async iterator/generator: Yields TikTokApi.playlist objects. Raises: InvalidResponseException: If TikTok returns an invalid response, or one we don't understand. @@ -100,7 +101,8 @@ async def playlists(self, count=20, cursor=0, **kwargs) -> Iterator[dict]: Example Usage: .. code-block:: python - user_data = await api.user(username='therock').playlist() + async for playlist in await api.user(username='therock').playlists(): + # do something """ sec_uid = getattr(self, "sec_uid", None) @@ -109,30 +111,30 @@ async def playlists(self, count=20, cursor=0, **kwargs) -> Iterator[dict]: found = 0 while found < count: - params = { - "secUid": sec_uid, - "count": 20, - "cursor": cursor, - } - - resp = await self.parent.make_request( - url="https://www.tiktok.com/api/user/playlist", - params=params, - headers=kwargs.get("headers"), - session_index=kwargs.get("session_index"), - ) - - if resp is None: - raise InvalidResponseException(resp, "TikTok returned an invalid response.") - - for playlist in resp.get("playList", []): - yield playlist - found += 1 - - if not resp.get("hasMore", False): - return - - cursor = resp.get("cursor") + params = { + "secUid": self.sec_uid, + "count": min(count, 20), + "cursor": cursor, + } + + resp = await self.parent.make_request( + url="https://www.tiktok.com/api/user/playlist", + params=params, + headers=kwargs.get("headers"), + session_index=kwargs.get("session_index"), + ) + + if resp is None: + raise InvalidResponseException(resp, "TikTok returned an invalid response.") + + for playlist in resp.get("playList", []): + yield self.parent.playlist(data=playlist) + found += 1 + + if not resp.get("hasMore", False): + return + + cursor = resp.get("cursor") async def videos(self, count=30, cursor=0, **kwargs) -> Iterator[Video]: diff --git a/TikTokApi/stealth/js/navigator_userAgent.py b/TikTokApi/stealth/js/navigator_userAgent.py index a89d2196..fa4f14f5 100644 --- a/TikTokApi/stealth/js/navigator_userAgent.py +++ b/TikTokApi/stealth/js/navigator_userAgent.py @@ -1,8 +1,16 @@ navigator_userAgent = """ // replace Headless references in default useragent -const current_ua = navigator.userAgent +const current_ua = navigator.userAgent; Object.defineProperty(Object.getPrototypeOf(navigator), 'userAgent', { - get: () => opts.navigator_user_agent || current_ua.replace('HeadlessChrome/', 'Chrome/') -}) - + get: () => { + try { + if (typeof opts !== 'undefined' && opts.navigator_user_agent) { + return opts.navigator_user_agent; + } + } catch (error) { + console.warn('Error accessing opts:', error); + } + return current_ua.replace('HeadlessChrome/', 'Chrome/'); + } +}); """ diff --git a/TikTokApi/tiktok.py b/TikTokApi/tiktok.py index 23c2e604..35b5372a 100644 --- a/TikTokApi/tiktok.py +++ b/TikTokApi/tiktok.py @@ -6,7 +6,7 @@ import time import json -from playwright.async_api import async_playwright +from playwright.async_api import async_playwright, TimeoutError from urllib.parse import urlencode, quote, urlparse from .stealth import stealth_async from .helpers import random_choice @@ -18,6 +18,7 @@ from .api.comment import Comment from .api.trending import Trending from .api.search import Search +from .api.playlist import Playlist from .exceptions import ( InvalidJSONException, @@ -55,6 +56,7 @@ class TikTokApi: comment = Comment trending = Trending search = Search + playlist = Playlist def __init__(self, logging_level: int = logging.WARN, logger_name: str = None): """ @@ -77,6 +79,7 @@ def __init__(self, logging_level: int = logging.WARN, logger_name: str = None): Comment.parent = self Trending.parent = self Search.parent = self + Playlist.parent = self def __create_logger(self, name: str, level: int = logging.DEBUG): """Create a logger for the class.""" @@ -143,6 +146,7 @@ async def __create_session( sleep_after: int = 1, cookies: dict = None, suppress_resource_load_types: list[str] = None, + timeout: int = 30000, ): """Create a TikTokPlaywrightSession""" if ms_token is not None: @@ -177,8 +181,20 @@ def handle_request(request): if request.resource_type in suppress_resource_load_types else route.continue_(), ) + + # Set the navigation timeout + page.set_default_navigation_timeout(timeout) await page.goto(url) + await page.goto(url) # hack: tiktok blocks first request not sure why, likely bot detection + + # by doing this, we are simulate scroll event using mouse to `avoid` bot detection + x, y = random.randint(0, 50), random.randint(0, 50) + a, b = random.randint(1, 50), random.randint(100, 200) + + await page.mouse.move(x, y) + await page.wait_for_load_state("networkidle") + await page.mouse.move(a, b) session = TikTokPlaywrightSession( context, @@ -213,7 +229,8 @@ async def create_sessions( cookies: list[dict] = None, suppress_resource_load_types: list[str] = None, browser: str = "chromium", - executable_path: str = None + executable_path: str = None, + timeout: int = 30000, ): """ Create sessions for use within the TikTokApi class. @@ -232,8 +249,9 @@ async def create_sessions( override_browser_args (list[dict]): A list of dictionaries containing arguments to pass to the browser. cookies (list[dict]): A list of cookies to use for the sessions, you can get these from your cookies after visiting TikTok. suppress_resource_load_types (list[str]): Types of resources to suppress playwright from loading, excluding more types will make playwright faster.. Types: document, stylesheet, image, media, font, script, textrack, xhr, fetch, eventsource, websocket, manifest, other. - browser (str): specify either firefox or chromium, default is chromium + browser (str): firefox, chromium, or webkit; default is chromium executable_path (str): Path to the browser executable + timeout (int): The timeout in milliseconds for page navigation Example Usage: .. code-block:: python @@ -271,6 +289,7 @@ async def create_sessions( sleep_after=sleep_after, cookies=random_choice(cookies), suppress_resource_load_types=suppress_resource_load_types, + timeout=timeout, ) for _ in range(num_sessions) ) @@ -363,7 +382,23 @@ async def run_fetch_script(self, url: str, headers: dict, **kwargs): async def generate_x_bogus(self, url: str, **kwargs): """Generate the X-Bogus header for a url""" _, session = self._get_session(**kwargs) - await session.page.wait_for_function("window.byted_acrawler !== undefined") + + max_attempts = 5 + attempts = 0 + while attempts < max_attempts: + attempts += 1 + try: + timeout_time = random.randint(5000, 20000) + await session.page.wait_for_function("window.byted_acrawler !== undefined", timeout=timeout_time) + break + except TimeoutError as e: + if attempts == max_attempts: + raise TimeoutError(f"Failed to load tiktok after {max_attempts} attempts, consider using a proxy") + + try_urls = ["https://www.tiktok.com/foryou", "https://www.tiktok.com", "https://www.tiktok.com/@tiktok", "https://www.tiktok.com/foryou"] + + await session.page.goto(random.choice(try_urls)) + result = await session.page.evaluate( f'() => {{ return window.byted_acrawler.frontierSign("{url}") }}' ) @@ -452,7 +487,7 @@ async def make_request( raise Exception("TikTokApi.run_fetch_script returned None") if result == "": - raise EmptyResponseException(result, "TikTok returned an empty response") + raise EmptyResponseException(result, "TikTok returned an empty response. They are detecting you're a bot, try some of these: headless=False, browser='webkit', consider using a proxy") try: data = json.loads(result) diff --git a/examples/comment_example.py b/examples/comment_example.py index cb7bbe4f..ccad61b5 100644 --- a/examples/comment_example.py +++ b/examples/comment_example.py @@ -8,7 +8,7 @@ async def get_comments(): async with TikTokApi() as api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) video = api.video(id=video_id) count = 0 async for comment in video.comments(count=30): diff --git a/examples/hashtag_example.py b/examples/hashtag_example.py index 0ddb94d4..dfc540bd 100644 --- a/examples/hashtag_example.py +++ b/examples/hashtag_example.py @@ -7,7 +7,7 @@ async def get_hashtag_videos(): async with TikTokApi() as api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) tag = api.hashtag(name="funny") async for video in tag.videos(count=30): print(video) diff --git a/examples/playlist_example.py b/examples/playlist_example.py new file mode 100644 index 00000000..be7c296b --- /dev/null +++ b/examples/playlist_example.py @@ -0,0 +1,24 @@ +from TikTokApi import TikTokApi +import asyncio +import os + +ms_token = os.environ.get( + "ms_token", None +) # set your own ms_token, think it might need to have visited a profile + + +async def user_example(): + async with TikTokApi() as api: + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + user = api.user("therock") + + async for playlist in user.playlists(count=3): + print(playlist) + print(playlist.name) + + async for video in playlist.videos(count=3): + print(video) + print(video.url) + +if __name__ == "__main__": + asyncio.run(user_example()) diff --git a/examples/search_example.py b/examples/search_example.py index 3391abfd..4da0a344 100644 --- a/examples/search_example.py +++ b/examples/search_example.py @@ -9,7 +9,7 @@ async def search_users(): async with TikTokApi() as api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) async for user in api.search.users("david teather", count=10): print(user) diff --git a/examples/sound_example.py b/examples/sound_example.py index 7a1853c3..2130651e 100644 --- a/examples/sound_example.py +++ b/examples/sound_example.py @@ -8,7 +8,7 @@ async def sound_videos(): async with TikTokApi() as api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) async for sound in api.sound(id=sound_id).videos(count=30): print(sound) print(sound.as_dict) diff --git a/examples/trending_example.py b/examples/trending_example.py index a743863a..31a33dd1 100644 --- a/examples/trending_example.py +++ b/examples/trending_example.py @@ -7,7 +7,7 @@ async def trending_videos(): async with TikTokApi() as api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) async for video in api.trending.videos(count=30): print(video) print(video.as_dict) diff --git a/examples/user_example.py b/examples/user_example.py index f2b9f72e..137da1f4 100644 --- a/examples/user_example.py +++ b/examples/user_example.py @@ -9,7 +9,7 @@ async def user_example(): async with TikTokApi() as api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) user = api.user("therock") user_data = await user.info() print(user_data) diff --git a/examples/video_example.py b/examples/video_example.py index 90f28606..6c8dcafa 100644 --- a/examples/video_example.py +++ b/examples/video_example.py @@ -9,7 +9,7 @@ async def get_video_example(): async with TikTokApi() as api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) video = api.video( url="https://www.tiktok.com/@davidteathercodes/video/7074717081563942186" ) diff --git a/imgs/webshare.png b/imgs/webshare.png new file mode 100644 index 0000000000000000000000000000000000000000..5cb3da4c6e31db953d6200692b7372d12e9f7523 GIT binary patch literal 10325 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 0 + assert playlist.cover_url is not None + assert playlist.as_dict is not None + +@pytest.mark.asyncio +async def test_playlist_videos(): + api = TikTokApi() + async with api: + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + playlist = api.playlist(id=playlist_id) + + count = 0 + async for video in playlist.videos(count=30): + count += 1 + + assert count >= 30 diff --git a/tests/test_search.py b/tests/test_search.py index 43231ac3..0b6fd654 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -8,7 +8,7 @@ async def test_users_single_page(): api = TikTokApi() async with api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) count = 0 async for user in api.search.users("therock", count=10): count += 1 @@ -20,7 +20,7 @@ async def test_users_single_page(): async def test_users_multi_page(): api = TikTokApi() async with api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) count = 0 async for user in api.search.users("therock", count=50): count += 1 diff --git a/tests/test_sound.py b/tests/test_sound.py index 4a506236..8df9553c 100644 --- a/tests/test_sound.py +++ b/tests/test_sound.py @@ -10,7 +10,7 @@ async def test_sound_videos(): api = TikTokApi() async with api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) sound = api.sound(id=song_id) video_count = 0 async for video in sound.videos(count=100): @@ -23,9 +23,9 @@ async def test_sound_videos(): async def test_sound_info(): api = TikTokApi() async with api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) sound = api.sound(id=song_id) await sound.info() assert sound.id == song_id assert sound.title == "Face Off - Dwayne Johnson" - assert sound.duration == 45 + assert sound.duration == 60 diff --git a/tests/test_trending.py b/tests/test_trending.py index 7a83445e..65519fb1 100644 --- a/tests/test_trending.py +++ b/tests/test_trending.py @@ -6,10 +6,10 @@ @pytest.mark.asyncio -async def test_user_info(): +async def test_trending(): api = TikTokApi() async with api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) count = 0 async for video in api.trending.videos(count=100): count += 1 diff --git a/tests/test_user.py b/tests/test_user.py index d0f07373..7ce2bfdc 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -13,7 +13,7 @@ async def test_user_info(): api = TikTokApi() async with api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) user = api.user(username=username) await user.info() @@ -25,7 +25,7 @@ async def test_user_info(): async def test_user_videos(): api = TikTokApi() async with api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) user = api.user(username=username, sec_uid=sec_uid, user_id=user_id) count = 0 @@ -38,7 +38,7 @@ async def test_user_videos(): async def test_user_likes(): api = TikTokApi() async with api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) user = api.user( username="publicliketest", sec_uid="MS4wLjABAAAAHjhwCIwmvzVZfRrDAZ2aZy74LciLnoyaPfM2rrX9N7bwbWMFuwTFG4YrByYvsH5c", @@ -54,7 +54,7 @@ async def test_user_likes(): async def test_user_playlists(): api = TikTokApi() async with api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) user = api.user(username="mrbeast") count = 0 diff --git a/tests/test_video.py b/tests/test_video.py index 625886f4..47469c0e 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -9,7 +9,7 @@ async def test_video_id_from_url(): api = TikTokApi() async with api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) expected_id = "7074717081563942186" video = api.video( @@ -28,7 +28,7 @@ async def test_video_id_from_url(): async def test_video_info(): api = TikTokApi() async with api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) video_id = "7074717081563942186" video = api.video( url="https://www.tiktok.com/@davidteathercodes/video/7074717081563942186" @@ -45,7 +45,7 @@ async def test_video_bytes(): pytest.skip("Not implemented yet") api = TikTokApi() async with api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) video_id = "7107272719166901550" video = api.video(id=video_id) @@ -57,7 +57,7 @@ async def test_video_bytes(): async def test_related_videos(): api = TikTokApi() async with api: - await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3) + await api.create_sessions(ms_tokens=[ms_token], num_sessions=1, sleep_after=3, browser=os.getenv("TIKTOK_BROWSER", "chromium")) video_id = "7107272719166901550" video = api.video(id=video_id) count = 0