diff --git a/docs/source/users/explanation.rst b/docs/source/users/explanation.rst index 87b354d..dcc51c7 100644 --- a/docs/source/users/explanation.rst +++ b/docs/source/users/explanation.rst @@ -7,6 +7,10 @@ but this does come with its limitations. TikTokPy grabs information in two steps: +.. warning:: + The following explanation is only valid for library versions prior to 0.2.0. This page will eventually be updated to + reflect the new behavior. + Grabbing Preloaded Content ========================== diff --git a/docs/source/users/usage.rst b/docs/source/users/usage.rst index 3bff060..80fd660 100644 --- a/docs/source/users/usage.rst +++ b/docs/source/users/usage.rst @@ -121,41 +121,6 @@ Given a :ref:`User` object, you can retrieve that creator's most recent videos. user page will be used for video data scraping. Specifying a limit can be useful if you only want the most recent videos. -Iterate Over Sorted Videos --------------------------- - -Unfortunately, this strategy is not perfect. TikTok does not provide a direct way to sort :ref:`Video`, so you will -only be able to perform the sorting on videos that are picked up by TikTokPy during scraping. More can be retrieved by -setting ``scroll_down_time`` to something like 10 seconds in the API constructor. The ``videos`` (async) iterator that -exists on :ref:`User` and :ref:`Challenge` objects contains a function called ``sorted_by()`` that has the same -signature as the builtin ``sorted()`` but is faster if you want to sort on :ref:`VideoStats` or ``create_time``. - -.. tabs:: - - .. code-tab:: py TikTokAPI - - from tiktokapipy.api import TikTokAPI - - def do_something(): - with TikTokAPI() as api: - user = api.user(user_tag) - for video in user.videos.sorted_by(key=lambda vid: vid.stats.digg_count, reverse=True): - ... - - .. code-tab:: py AsyncTikTokAPI - - from tiktokapipy.async_api import AsyncTikTokAPI - - async def do_something(): - async with AsyncTikTokAPI() as api: - user = await api.user(user_tag) - async for video in user.videos.sorted_by(key=lambda vid: vid.stats.digg_count, reverse=True): - ... - -.. note:: - All other video data besides the unique ID and stats are grabbed at iteration time, so if you would like to sort on - something else you should just go with ``sorted()``. This helps keep the memory footprint low. - Iterate Over Popular Videos Tagged with a Challenge --------------------------------------------------- @@ -184,7 +149,6 @@ TikTok refers to hashtags as "Challenges" internally. You can iterate over popul async for video in challenge.videos: ... -You can also sort these by create time with ``challenge.videos.sorted_by(lambda vid: vid.create_time)``. .. note:: By default, the number of videos that can be iterated over is not limited. This can be changed by specifying a diff --git a/pyproject.toml b/pyproject.toml index 2408763..a584740 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tiktokapipy" -version = "0.2.0post2" +version = "0.2.1" authors = [ { name="Russell Newton", email="russell.newton01@gmail.com" }, ] diff --git a/src/tiktokapipy/__init__.py b/src/tiktokapipy/__init__.py index 00ef46f..9f1bd24 100644 --- a/src/tiktokapipy/__init__.py +++ b/src/tiktokapipy/__init__.py @@ -1,3 +1,6 @@ +from collections import defaultdict + + class TikTokAPIError(Exception): """Raised when the API encounters an error""" @@ -8,43 +11,46 @@ class TikTokAPIWarning(RuntimeWarning): pass -ERROR_CODES = { - 0: "OK", - 450: "CLIENT_PAGE_ERROR", - 10000: "VERIFY_CODE", - 10101: "SERVER_ERROR_NOT_500", - 10102: "USER_NOT_LOGIN", - 10111: "NET_ERROR", - 10113: "SHARK_SLIDE", - 10114: "SHARK_BLOCK", - 10119: "LIVE_NEED_LOGIN", - 10202: "USER_NOT_EXIST", - 10203: "MUSIC_NOT_EXIST", - 10204: "VIDEO_NOT_EXIST", - 10205: "HASHTAG_NOT_EXIST", - 10208: "EFFECT_NOT_EXIST", - 10209: "HASHTAG_BLACK_LIST", - 10210: "LIVE_NOT_EXIST", - 10211: "HASHTAG_SENSITIVITY_WORD", - 10212: "HASHTAG_UNSHELVE", - 10213: "VIDEO_LOW_AGE_M", - 10214: "VIDEO_LOW_AGE_T", - 10215: "VIDEO_ABNORMAL", - 10216: "VIDEO_PRIVATE_BY_USER", - 10217: "VIDEO_FIRST_REVIEW_UNSHELVE", - 10218: "MUSIC_UNSHELVE", - 10219: "MUSIC_NO_COPYRIGHT", - 10220: "VIDEO_UNSHELVE_BY_MUSIC", - 10221: "USER_BAN", - 10222: "USER_PRIVATE", - 10223: "USER_FTC", - 10224: "GAME_NOT_EXIST", - 10225: "USER_UNIQUE_SENSITIVITY", - 10227: "VIDEO_NEED_RECHECK", - 10228: "VIDEO_RISK", - 10229: "VIDEO_R_MASK", - 10230: "VIDEO_RISK_MASK", - 10231: "VIDEO_GEOFENCE_BLOCK", - 10404: "FYP_VIDEO_LIST_LIMIT", - "undefined": "MEDIA_ERROR", -} +ERROR_CODES = defaultdict( + lambda: "Unknown Error", + { + 0: "OK", + 450: "CLIENT_PAGE_ERROR", + 10000: "VERIFY_CODE", + 10101: "SERVER_ERROR_NOT_500", + 10102: "USER_NOT_LOGIN", + 10111: "NET_ERROR", + 10113: "SHARK_SLIDE", + 10114: "SHARK_BLOCK", + 10119: "LIVE_NEED_LOGIN", + 10202: "USER_NOT_EXIST", + 10203: "MUSIC_NOT_EXIST", + 10204: "VIDEO_NOT_EXIST", + 10205: "HASHTAG_NOT_EXIST", + 10208: "EFFECT_NOT_EXIST", + 10209: "HASHTAG_BLACK_LIST", + 10210: "LIVE_NOT_EXIST", + 10211: "HASHTAG_SENSITIVITY_WORD", + 10212: "HASHTAG_UNSHELVE", + 10213: "VIDEO_LOW_AGE_M", + 10214: "VIDEO_LOW_AGE_T", + 10215: "VIDEO_ABNORMAL", + 10216: "VIDEO_PRIVATE_BY_USER", + 10217: "VIDEO_FIRST_REVIEW_UNSHELVE", + 10218: "MUSIC_UNSHELVE", + 10219: "MUSIC_NO_COPYRIGHT", + 10220: "VIDEO_UNSHELVE_BY_MUSIC", + 10221: "USER_BAN", + 10222: "USER_PRIVATE", + 10223: "USER_FTC", + 10224: "GAME_NOT_EXIST", + 10225: "USER_UNIQUE_SENSITIVITY", + 10227: "VIDEO_NEED_RECHECK", + 10228: "VIDEO_RISK", + 10229: "VIDEO_R_MASK", + 10230: "VIDEO_RISK_MASK", + 10231: "VIDEO_GEOFENCE_BLOCK", + 10404: "FYP_VIDEO_LIST_LIMIT", + "undefined": "MEDIA_ERROR", + }, +) diff --git a/src/tiktokapipy/models/video.py b/src/tiktokapipy/models/video.py index 642255b..aa67324 100644 --- a/src/tiktokapipy/models/video.py +++ b/src/tiktokapipy/models/video.py @@ -4,11 +4,9 @@ from datetime import datetime from functools import cached_property -from io import BytesIO from typing import Any, ForwardRef, List, Optional, Union from playwright.async_api import BrowserContext as AsyncBrowserContext -from playwright.sync_api import BrowserContext as SyncBrowserContext from pydantic import AliasChoices, Field, computed_field from tiktokapipy import TikTokAPIError from tiktokapipy.models import CamelCaseModel, TitleCaseModel @@ -84,7 +82,7 @@ class MusicData(CamelCaseModel): id: int title: str - play_url: str + play_url: Optional[str] = None author_name: Optional[str] = None duration: int original: bool @@ -256,81 +254,11 @@ def creator(self) -> Union[DeferredUserGetterAsync, DeferredUserGetterSync]: else: return DeferredUserGetterSync(self._api, unique_id) - async def download_async(self) -> BytesIO: - if self.image_post: - raise TikTokAPIError( - "Downloading slide shows is not directly supported yet." - ) - if self._api is None: - raise TikTokAPIError( - "A TikTokAPI must be attached to video._api before retrieving creator data." - ) - if isinstance(self._api.context, SyncBrowserContext): - raise TikTokAPIError( - "Attempting to use TikTokAPI in an asynchronous context. Use `download_sync()` instead." - ) - - from playwright.async_api import Page - - page: Page = await self._api.context.new_page() - response = await page.goto( - self.video.download_addr, referer="https://www.tiktok.com" - ) - return BytesIO(await response.body()) - - def download_sync(self) -> BytesIO: - if self.image_post: - raise TikTokAPIError( - "Downloading slide shows is not directly supported yet." - ) - if self._api is None: - raise TikTokAPIError( - "A TikTokAPI must be attached to video._api before retrieving creator data." - ) - if isinstance(self._api.context, AsyncBrowserContext): - raise TikTokAPIError( - "Attempting to use AsyncTikTokAPI in a synchronous context. Use `await download_async()` instead." - ) - - from playwright.sync_api import Page - - page: Page = self._api.context.new_page() - page.add_init_script( - """ - if (navigator.webdriver === false) { - // Post Chrome 89.0.4339.0 and already good - } else if (navigator.webdriver === undefined) { - // Pre Chrome 89.0.4339.0 and already good - } else { - // Pre Chrome 88.0.4291.0 and needs patching - delete Object.getPrototypeOf(navigator).webdriver - } - """ - ) - # response = page.goto(self.video.download_addr) - print(page.context.cookies()) - if len(page.context.cookies()) == 0: - page.goto("https://www.tiktok.com") - page.reload() - page.wait_for_timeout(5000) - print(page.context.cookies()) - cookie_header = "; ".join( - f"{cookie['name']}={cookie['value']}" - for cookie in page.context.cookies() - if cookie["domain"] == ".tiktok.com" - ) - response2 = page.request.get( - self.video.download_addr, - headers={ - "Referer": "https://www.tiktok.com", - "Sec-Fetch-Dest": "video", - "Sec-Fetch-Mode": "no-cors", - "Sec-Fetch-Site": "same-site", - "Cookie": cookie_header, - }, - ) - page.close() - return BytesIO(response2.body()) + @computed_field(repr=False) + @cached_property + def url(self) -> str: + """The url to the video on TikTok.""" + return video_link(self.id) del Challenge, LightChallenge, Comment, LightUser, User, UserStats