From ff49ce8a2fbd4dbe3322946702e96ba33d3b089c Mon Sep 17 00:00:00 2001 From: Moldachev Sergey Date: Sun, 24 Oct 2021 18:25:19 +0300 Subject: [PATCH] Initial release --- .gitignore | 4 + LICENSE | 21 ++ Pipfile | 13 + Pipfile.lock | 157 +++++++++ README.md | 40 +++ client/api.py | 299 ++++++++++++++++++ client/client.py | 13 + examples/download_all_user_videos.py | 51 +++ examples/download_trending_videos.py | 50 +++ examples/download_videos_by_effect_id.py | 49 +++ examples/download_videos_by_hashtag_id.py | 49 +++ examples/download_videos_by_music_id.py | 49 +++ examples/get_category_list_with_pagination.py | 52 +++ examples/get_comments_for_video.py | 24 ++ examples/get_effect_info.py | 36 +++ examples/get_hashtag_info.py | 20 ++ examples/get_music_info.py | 20 ++ examples/get_user_followers.py | 25 ++ examples/get_user_followings.py | 25 ++ examples/get_user_info.py | 34 ++ examples/get_video_info.py | 40 +++ examples/search_by_keyword.py | 67 ++++ setup.py | 24 ++ 23 files changed, 1162 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 README.md create mode 100644 client/api.py create mode 100644 client/client.py create mode 100644 examples/download_all_user_videos.py create mode 100644 examples/download_trending_videos.py create mode 100644 examples/download_videos_by_effect_id.py create mode 100644 examples/download_videos_by_hashtag_id.py create mode 100644 examples/download_videos_by_music_id.py create mode 100644 examples/get_category_list_with_pagination.py create mode 100644 examples/get_comments_for_video.py create mode 100644 examples/get_effect_info.py create mode 100644 examples/get_hashtag_info.py create mode 100644 examples/get_music_info.py create mode 100644 examples/get_user_followers.py create mode 100644 examples/get_user_followings.py create mode 100644 examples/get_user_info.py create mode 100644 examples/get_video_info.py create mode 100644 examples/search_by_keyword.py create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9643c9c --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +build +dist +tokapi_client.egg-info \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6104bc3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Live Code Stream + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..2666cc0 --- /dev/null +++ b/Pipfile @@ -0,0 +1,13 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +httpx = "*" + +[dev-packages] +pytest = "*" + +[requires] +python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..88b32da --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,157 @@ +{ + "_meta": { + "hash": { + "sha256": "fefaa4d33b955433f19d36a8a4a1541cbc2dd0b31ce1d66131f410553f58b6c5" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "anyio": { + "hashes": [ + "sha256:4fd09a25ab7fa01d34512b7249e366cd10358cdafc95022c7ff8c8f8a5026d66", + "sha256:67da67b5b21f96b9d3d65daa6ea99f5d5282cb09f50eb4456f8fb51dffefc3ff" + ], + "markers": "python_full_version >= '3.6.2'", + "version": "==3.3.4" + }, + "certifi": { + "hashes": [ + "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", + "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569" + ], + "version": "==2021.10.8" + }, + "charset-normalizer": { + "hashes": [ + "sha256:e019de665e2bcf9c2b64e2e5aa025fa991da8720daa3c1138cadd2fd1856aed0", + "sha256:f7af805c321bfa1ce6714c51f254e0d5bb5e5834039bc17db7ebe3a4cec9492b" + ], + "markers": "python_full_version >= '3.5.0'", + "version": "==2.0.7" + }, + "h11": { + "hashes": [ + "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6", + "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042" + ], + "markers": "python_version >= '3.6'", + "version": "==0.12.0" + }, + "httpcore": { + "hashes": [ + "sha256:036f960468759e633574d7c121afba48af6419615d36ab8ede979f1ad6276fa3", + "sha256:369aa481b014cf046f7067fddd67d00560f2f00426e79569d99cb11245134af0" + ], + "markers": "python_version >= '3.6'", + "version": "==0.13.7" + }, + "httpx": { + "hashes": [ + "sha256:09606d630f070d07f9ff28104fbcea429ea0014c1e89ac90b4d8de8286c40e7b", + "sha256:33af5aad9bdc82ef1fc89219c1e36f5693bf9cd0ebe330884df563445682c0f8" + ], + "index": "pypi", + "version": "==0.20.0" + }, + "idna": { + "hashes": [ + "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", + "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d" + ], + "version": "==3.3" + }, + "rfc3986": { + "extras": [ + "idna2008" + ], + "hashes": [ + "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835", + "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97" + ], + "version": "==1.5.0" + }, + "sniffio": { + "hashes": [ + "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", + "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" + ], + "markers": "python_full_version >= '3.5.0'", + "version": "==1.2.0" + } + }, + "develop": { + "attrs": { + "hashes": [ + "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1", + "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.2.0" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "packaging": { + "hashes": [ + "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7", + "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14" + ], + "markers": "python_version >= '3.6'", + "version": "==21.0" + }, + "pluggy": { + "hashes": [ + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" + ], + "markers": "python_version >= '3.6'", + "version": "==1.0.0" + }, + "py": { + "hashes": [ + "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3", + "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.10.0" + }, + "pyparsing": { + "hashes": [ + "sha256:001cad8d467e7a9248ef9fd513f5c0d39afcbcb9a43684101853bd0ab962e479", + "sha256:d487599e9fb0dc36bee6b5c183c6fc5bd372ce667736f3d430ab7d842a54a35a" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.0" + }, + "pytest": { + "hashes": [ + "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", + "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" + ], + "index": "pypi", + "version": "==6.2.5" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..49072f5 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ + +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/mtokapi/tokapi-client/issues) + + +The simple http client for [TokApi](https://rapidapi.com/Sonjik/api/tokapi-mobile-version) tiktok mobile API + +## Installation + +```shell +pip install tokapi-client +``` + +## Usage + +```shell +api = TokApi('YOUR_RAPID_API_KEY') + +# Let's find some users by search query with pagination +keyword = 'nike' +offset = 0 +for i in range(0, 3): + result = api.search_user_by_keyword(keyword, offset=offset) + data = result.json() + + for user in data['user_list']: + info = user['user_info'] + print('Nickname: {}, region: {}'.format(info['nickname'], + info['region'])) + + offset = data['cursor'] +``` + +## Examples + +You can find more complex usage examples in [examples](examples) folder + +## Legal + +This code is in no way affiliated with, authorized, maintained, sponsored or endorsed by TikTok +or any of its affiliates or subsidiaries. This is an independent and unofficial API. Use at your own risk. \ No newline at end of file diff --git a/client/api.py b/client/api.py new file mode 100644 index 0000000..7614f4f --- /dev/null +++ b/client/api.py @@ -0,0 +1,299 @@ +import httpx +from httpx import Response + +from client.client import TokApiClient + + +class TokApi(object): + def __init__(self, api_key: str, rapid_host: str = 'https://tokapi-mobile-version.p.rapidapi.com', + proxy: httpx.Proxy = None): + self.http_client = TokApiClient(api_key=api_key, rapid_host=rapid_host, proxy=proxy) + + def get_categories(self, count: int = 10, offset: int = 0, region: str = 'US') -> Response: + """ + :param count: number of items, a maximum value is 50 (maybe changed by tiktok in future) + :param offset: number of item which will be skipped from start + :param region: region + :return: Response object + """ + params = { + 'count': count, + 'offset': offset, + 'region': region, + } + + response = self.http_client.execute('GET', '/v1/category', params) + + return response + + def get_trending_feed(self, pull_type: int = 0, region: str = 'US', device_config: str = '', + cookie: str = '') -> Response: + """ + :param pull_type: type of loading feed (0 - Initial loading, 2 - Load more, 8 - Reload) + :param region: region + :param device_config: device configuration + :param cookie: cookie + :return: Response object + """ + params = { + 'pull_type': pull_type, + 'region': region + } + + headers = {'x-device': device_config, 'x-cookie': cookie} if device_config and cookie else None + response = self.http_client.execute('GET', '/v1/feed/recommended', params=params, headers=headers) + + return response + + def get_hashtag_info_by_id(self, hashtag_id: str) -> Response: + """ + :param hashtag_id: hashtag id + :return: Response object + """ + + response = self.http_client.execute('GET', '/v1/hashtag/{}'.format(hashtag_id)) + + return response + + def get_videos_by_hashtag_id(self, hashtag_id: str, count: int = 20, offset: int = 0) -> Response: + """ + :param hashtag_id: hashtag id + :param count: number of items, a maximum value is 20 (can be changed by tiktok in future) + :param offset: number of item which will be skipped from start + :return: Response object + """ + params = { + 'count': count, + 'offset': offset, + } + + response = self.http_client.execute('GET', '/v1/hashtag/posts/{}'.format(hashtag_id), params) + + return response + + def get_music_info_by_id(self, music_id: str) -> Response: + """ + :param music_id: music id + :return: Response object + """ + + response = self.http_client.execute('GET', '/v1/music/{}'.format(music_id)) + + return response + + def get_videos_by_music_id(self, music_id: str, count: int = 20, offset: int = 0) -> Response: + """ + :param music_id: music id + :param count: number of items, a maximum value is 20 (can be changed by tiktok in future) + :param offset: number of item which will be skipped from start + :return: Response object + """ + params = { + 'count': count, + 'offset': offset, + } + + response = self.http_client.execute('GET', '/v1/music/posts/{}'.format(music_id), params) + + return response + + def get_video_info_by_id(self, video_id: str) -> Response: + """ + :param video_id: video id + :return: Response object + """ + + response = self.http_client.execute('GET', '/v1/post/{}'.format(video_id)) + + return response + + def get_video_info_by_web_link(self, video_web_link: str) -> Response: + """ + :param video_web_link: video web link + :return: Response object + """ + + params = { + 'video_url': video_web_link + } + + response = self.http_client.execute('GET', '/v1/post', params) + + return response + + def get_user_videos(self, user_id: str, count: int = 20, offset: int = 0) -> Response: + """ + :param user_id: tiktok user id + :param count: number of items, a maximum value is 50 (can be changed by tiktok in future) + :param offset: number of item which will be skipped from start + :return: Response object + """ + params = { + 'count': count, + 'offset': offset, + } + + response = self.http_client.execute('GET', '/v1/post/user/{}/posts'.format(user_id), params) + + return response + + def get_comments_by_video_id(self, video_id: str, count: int = 20, offset: int = 0) -> Response: + """ + :param video_id: video id + :param count: number of items + :param offset: number of item which will be skipped from start + :return: Response object + """ + params = { + 'count': count, + 'offset': offset, + } + + response = self.http_client.execute('GET', '/v1/post/{}/comments'.format(video_id), params) + + return response + + def search_user_by_keyword(self, keyword: str, count: int = 10, offset: int = 0) -> Response: + """ + :param keyword: search text query + :param count: number of items + :param offset: number of item which will be skipped from start + :return: Response object + """ + params = { + 'keyword': keyword, + 'count': count, + 'offset': offset, + } + + response = self.http_client.execute('GET', '/v1/search/user', params) + + return response + + def search_hashtags_by_keyword(self, keyword: str, count: int = 10, offset: int = 0) -> Response: + """ + :param keyword: search text query + :param count: number of items + :param offset: number of item which will be skipped from start + :return: Response object + """ + params = { + 'keyword': keyword, + 'count': count, + 'offset': offset, + } + + response = self.http_client.execute('GET', '/v1/search/hashtag', params) + + return response + + def search_music_by_keyword(self, keyword: str, count: int = 10, offset: int = 0) -> Response: + """ + :param keyword: search text query + :param count: number of items + :param offset: number of item which will be skipped from start + :return: Response object + """ + params = { + 'keyword': keyword, + 'count': count, + 'offset': offset, + } + + response = self.http_client.execute('GET', '/v1/search/music', params) + + return response + + def get_videos_by_effect_id(self, effect_id: str, count: int = 20, offset: int = 0) -> Response: + """ + :param effect_id: effect id + :param count: number of items + :param offset: number of item which will be skipped from start + :return: Response object + """ + params = { + 'count': count, + 'offset': offset, + } + + response = self.http_client.execute('GET', '/v1/sticker/posts/1108584'.format(effect_id), params) + + return response + + def get_effect_info_by_id(self, effect_id: str) -> Response: + """ + :param effect_id: effect id + :return: Response object + """ + + response = self.http_client.execute('GET', '/v1/sticker/{}'.format(effect_id)) + + return response + + def get_effect_info_by_ids(self, effect_ids: list) -> Response: + """ + :param effect_ids: effect ids + :return: Response object + """ + + params = { + 'sticker_ids': ','.join(effect_ids) + } + + response = self.http_client.execute('GET', '/v1/sticker', params) + + return response + + def get_user_info_by_id(self, user_id: str) -> Response: + """ + :param user_id: user id + :return: Response object + """ + + response = self.http_client.execute('GET', '/v1/user/{}'.format(user_id)) + + return response + + def get_user_followers(self, user_id: str, count: int = 20, offset: int = 0) -> Response: + """ + :param user_id: user id + :param count: number of items + :param offset: number of item which will be skipped from start + :return: Response object + """ + + params = { + 'count': count, + 'offset': offset + } + + response = self.http_client.execute('GET', '/v1/user/{}/followers'.format(user_id), params) + + return response + + def get_user_followings(self, user_id: str, count: int = 20, offset: int = 0) -> Response: + """ + :param user_id: user id + :param count: number of items + :param offset: number of item which will be skipped from start + :return: Response object + """ + + params = { + 'count': count, + 'offset': offset, + } + + response = self.http_client.execute('GET', '/v1/user/{}/followings'.format(user_id), params) + + return response + + def get_user_qr_code(self, user_id: str) -> Response: + """ + :param user_id: user id + :return: Response object + """ + + response = self.http_client.execute('GET', '/v1/user/{}/qr_code'.format(user_id)) + + return response diff --git a/client/client.py b/client/client.py new file mode 100644 index 0000000..7d356e4 --- /dev/null +++ b/client/client.py @@ -0,0 +1,13 @@ +import httpx + + +class TokApiClient(object): + def __init__(self, api_key: str, rapid_host: str, proxy: httpx.Proxy = None): + default_headers = { + 'x-rapidapi-host': rapid_host.replace('http://', '').replace('https://', ''), + 'x-rapidapi-key': api_key + } + self.http_client = httpx.Client(base_url=rapid_host, headers=default_headers, proxies=proxy) + + def execute(self, method: str, path: str, params: dict = None, headers: dict = None): + return self.http_client.request(url=path, method=method, params=params, headers=headers) diff --git a/examples/download_all_user_videos.py b/examples/download_all_user_videos.py new file mode 100644 index 0000000..d5107af --- /dev/null +++ b/examples/download_all_user_videos.py @@ -0,0 +1,51 @@ +import concurrent.futures +import os + +import httpx +from httpx import Response + +from client.api import TokApi + + +def example(user_id: str, dest_folder: str): + """ + Download all user videos to folder + """ + api = TokApi('YOUR_RAPID_API_KEY') + + dest_folder = os.path.join(dest_folder, user_id) + if not os.path.exists(dest_folder): + os.makedirs(dest_folder) + + offset = 0 + has_more = 1 + print('========== START DOWNLOADING VIDEOS FOR USER: {} ========== '.format(user_id)) + + videos = [] + while has_more == 1: + result: Response = api.get_user_videos(user_id, offset=offset) + + data = result.json() + videos.extend(data['aweme_list']) + + has_more = data['has_more'] + offset = data['max_cursor'] + + with concurrent.futures.ThreadPoolExecutor(max_workers=6) as executor: + for video_info in videos: + executor.submit(download_video, dest_folder, video_info) + + +def download_video(dest_folder: str, video_info: dict): + filename = '{}.mp4'.format(video_info['aweme_id']) + file_path = os.path.join(dest_folder, filename) + + with httpx.stream("GET", video_info['video']['play_addr']['url_list'][0]) as response: + print("Saving to", os.path.abspath(file_path)) + with open(file_path, 'wb') as f: + for chunk in response.iter_bytes(chunk_size=1024 * 8): + f.write(chunk) + + +if __name__ == "__main__": + example('6784563164518679557', os.path.dirname(os.path.abspath(__file__))) diff --git a/examples/download_trending_videos.py b/examples/download_trending_videos.py new file mode 100644 index 0000000..77379b2 --- /dev/null +++ b/examples/download_trending_videos.py @@ -0,0 +1,50 @@ +import concurrent.futures +import os + +import httpx +from httpx import Response + +from client.api import TokApi + + +def example(dest_folder: str): + """ + Download trending videos to folder + """ + api = TokApi('YOUR_RAPID_API_KEY') + + dest_folder = os.path.join(dest_folder, 'trending') + if not os.path.exists(dest_folder): + os.makedirs(dest_folder) + + print('========== START DOWNLOADING TRENDING VIDEOS ========== ') + + videos = [] + device = '' + cookie = '' + for i in range(0, 5): + result: Response = api.get_trending_feed(0 if i == 0 else 2, device_config=device, cookie=cookie) + + data = result.json() + videos.extend(data['aweme_list']) + device = result.headers.get('x-device') + cookie = result.headers.get('x-cookie') + + with concurrent.futures.ThreadPoolExecutor(max_workers=6) as executor: + for video_info in videos: + executor.submit(download_video, dest_folder, video_info) + + +def download_video(dest_folder: str, video_info: dict): + filename = '{}.mp4'.format(video_info['aweme_id']) + file_path = os.path.join(dest_folder, filename) + + with httpx.stream("GET", video_info['video']['play_addr']['url_list'][0]) as response: + print("Saving to", os.path.abspath(file_path)) + with open(file_path, 'wb') as f: + for chunk in response.iter_bytes(chunk_size=1024 * 8): + f.write(chunk) + + +if __name__ == "__main__": + example(os.path.dirname(os.path.abspath(__file__))) diff --git a/examples/download_videos_by_effect_id.py b/examples/download_videos_by_effect_id.py new file mode 100644 index 0000000..4c47f28 --- /dev/null +++ b/examples/download_videos_by_effect_id.py @@ -0,0 +1,49 @@ +import concurrent.futures +import os + +import httpx +from httpx import Response + +from client.api import TokApi + + +def example(effect_id: str, dest_folder: str): + """ + Download videos with effect to folder + """ + api = TokApi('YOUR_RAPID_API_KEY') + + dest_folder = os.path.join(dest_folder, effect_id) + if not os.path.exists(dest_folder): + os.makedirs(dest_folder) + + print('========== START DOWNLOADING VIDEOS BY EFFECT: {} ========== '.format(effect_id)) + + offset = 0 + videos = [] + for i in range(0, 3): + result: Response = api.get_videos_by_effect_id(effect_id, offset=offset) + + data = result.json() + videos.extend(data['aweme_list']) + + offset = data['cursor'] + + with concurrent.futures.ThreadPoolExecutor(max_workers=6) as executor: + for video_info in videos: + executor.submit(download_video, dest_folder, video_info) + + +def download_video(dest_folder: str, video_info: dict): + filename = '{}.mp4'.format(video_info['aweme_id']) + file_path = os.path.join(dest_folder, filename) + + with httpx.stream("GET", video_info['video']['play_addr']['url_list'][0]) as response: + print("Saving to", os.path.abspath(file_path)) + with open(file_path, 'wb') as f: + for chunk in response.iter_bytes(chunk_size=1024 * 8): + f.write(chunk) + + +if __name__ == "__main__": + example('1108584', os.path.dirname(os.path.abspath(__file__))) diff --git a/examples/download_videos_by_hashtag_id.py b/examples/download_videos_by_hashtag_id.py new file mode 100644 index 0000000..642d292 --- /dev/null +++ b/examples/download_videos_by_hashtag_id.py @@ -0,0 +1,49 @@ +import concurrent.futures +import os + +import httpx +from httpx import Response + +from client.api import TokApi + + +def example(hashtag_id: str, dest_folder: str): + """ + Download videos by specific hashtag to folder + """ + api = TokApi('YOUR_RAPID_API_KEY') + + dest_folder = os.path.join(dest_folder, hashtag_id) + if not os.path.exists(dest_folder): + os.makedirs(dest_folder) + + print('========== START DOWNLOADING VIDEOS BY HASHTAG: {} ========== '.format(hashtag_id)) + + offset = 0 + videos = [] + for i in range(0, 3): + result: Response = api.get_videos_by_hashtag_id(hashtag_id, offset=offset) + + data = result.json() + videos.extend(data['aweme_list']) + + offset = data['cursor'] + + with concurrent.futures.ThreadPoolExecutor(max_workers=6) as executor: + for video_info in videos: + executor.submit(download_video, dest_folder, video_info) + + +def download_video(dest_folder: str, video_info: dict): + filename = '{}.mp4'.format(video_info['aweme_id']) + file_path = os.path.join(dest_folder, filename) + + with httpx.stream("GET", video_info['video']['play_addr']['url_list'][0]) as response: + print("Saving to", os.path.abspath(file_path)) + with open(file_path, 'wb') as f: + for chunk in response.iter_bytes(chunk_size=1024 * 8): + f.write(chunk) + + +if __name__ == "__main__": + example('2878999', os.path.dirname(os.path.abspath(__file__))) diff --git a/examples/download_videos_by_music_id.py b/examples/download_videos_by_music_id.py new file mode 100644 index 0000000..8dcf41f --- /dev/null +++ b/examples/download_videos_by_music_id.py @@ -0,0 +1,49 @@ +import concurrent.futures +import os + +import httpx +from httpx import Response + +from client.api import TokApi + + +def example(music_id: str, dest_folder: str): + """ + Download videos by specific hashtag to folder + """ + api = TokApi('YOUR_RAPID_API_KEY') + + dest_folder = os.path.join(dest_folder, music_id) + if not os.path.exists(dest_folder): + os.makedirs(dest_folder) + + print('========== START DOWNLOADING VIDEOS BY MUSIC: {} ========== '.format(music_id)) + + offset = 0 + videos = [] + for i in range(0, 3): + result: Response = api.get_videos_by_music_id(music_id, offset=offset) + + data = result.json() + videos.extend(data['aweme_list']) + + offset = data['cursor'] + + with concurrent.futures.ThreadPoolExecutor(max_workers=6) as executor: + for video_info in videos: + executor.submit(download_video, dest_folder, video_info) + + +def download_video(dest_folder: str, video_info: dict): + filename = '{}.mp4'.format(video_info['aweme_id']) + file_path = os.path.join(dest_folder, filename) + + with httpx.stream("GET", video_info['video']['play_addr']['url_list'][0]) as response: + print("Saving to", os.path.abspath(file_path)) + with open(file_path, 'wb') as f: + for chunk in response.iter_bytes(chunk_size=1024 * 8): + f.write(chunk) + + +if __name__ == "__main__": + example('6928004115846924290', os.path.dirname(os.path.abspath(__file__))) diff --git a/examples/get_category_list_with_pagination.py b/examples/get_category_list_with_pagination.py new file mode 100644 index 0000000..cda788f --- /dev/null +++ b/examples/get_category_list_with_pagination.py @@ -0,0 +1,52 @@ +from httpx import Response + +from client.api import TokApi + + +def example(): + """ + Load trending categories for default US region + """ + api = TokApi('YOUR_RAPID_API_KEY') + + offset = 0 + for i in range(0, 3): + result: Response = api.get_categories(offset=offset) + data = result.json() + + print('========== START PAGE ========== items count: {}'.format(len(data['category_list']))) + for category in data['category_list']: + print('Category type: {}'.format(category['desc'])) + if category['category_type'] == 3: + print_effect_info(category) + elif category['category_type'] == 1: + print_music_info(category) + elif category['category_type'] == 0: + print_challenge_info(category) + print('=========== END PAGE ===========\n') + + # If we have more results then use cursor param from response + if data['has_more'] == 1: + offset = data['cursor'] + else: + break + + +def print_effect_info(category: dict): + print('Effect name : {}, user count: {}'.format(category['effect_info']['name'], + category['effect_info']['user_count'])) + + +def print_music_info(category: dict): + print('Music name : {}, user count: {}'.format(category['music_info']['title'], + category['music_info']['user_count'])) + + +def print_challenge_info(category: dict): + print('Challenge name : {}, user count: {}, view count: {}'.format(category['challenge_info']['cha_name'], + category['challenge_info']['user_count'], + category['challenge_info']['view_count'])) + + +if __name__ == "__main__": + example() diff --git a/examples/get_comments_for_video.py b/examples/get_comments_for_video.py new file mode 100644 index 0000000..a21b483 --- /dev/null +++ b/examples/get_comments_for_video.py @@ -0,0 +1,24 @@ +from client.api import TokApi + + +def example(video_id: str): + """ + Print comments for video + """ + api = TokApi('YOUR_RAPID_API_KEY') + + offset = 0 + for i in range(0, 3): + result = api.get_comments_by_video_id(video_id, offset=offset) + data = result.json() + + for comment in data['comments']: + print('Comment: {}, likes: {}, author: {}, author region: {}'.format(comment['text'], + comment['digg_count'], + comment['user']['nickname'], + comment['user']['region'])) + offset = data['cursor'] + + +if __name__ == "__main__": + example('6977747303692078337') diff --git a/examples/get_effect_info.py b/examples/get_effect_info.py new file mode 100644 index 0000000..b517152 --- /dev/null +++ b/examples/get_effect_info.py @@ -0,0 +1,36 @@ +from client.api import TokApi + + +def example_by_id(effect_id: str): + """ + Print effect info by id + """ + api = TokApi('YOUR_RAPID_API_KEY') + + result = api.get_effect_info_by_id(effect_id) + data = result.json() + + info = data['sticker_infos'][0] + print('Name: {}, user count: {}, views: {}'.format(info['name'], + info['user_count'], + info['vv_count'])) + + +def example_by_ids(ids: list): + """ + Print effect infos by ids + """ + api = TokApi('YOUR_RAPID_API_KEY') + + result = api.get_effect_info_by_ids(ids) + data = result.json() + + for sticker in data['sticker_infos']: + print('Name: {}, user count: {}, views: {}'.format(sticker['name'], + sticker['user_count'], + sticker['vv_count'])) + + +if __name__ == "__main__": + example_by_id('1108584') + example_by_ids(['1108584', '1108584']) diff --git a/examples/get_hashtag_info.py b/examples/get_hashtag_info.py new file mode 100644 index 0000000..e63353c --- /dev/null +++ b/examples/get_hashtag_info.py @@ -0,0 +1,20 @@ +from client.api import TokApi + + +def example(): + """ + Print hashtag info + """ + api = TokApi('YOUR_RAPID_API_KEY') + + result = api.get_hashtag_info_by_id('2878999') + data = result.json() + + info = data['ch_info'] + print('Hashtag name: {}, user count: {}, views count: {}'.format(info['cha_name'], + info['user_count'], + info['view_count'])) + + +if __name__ == "__main__": + example() diff --git a/examples/get_music_info.py b/examples/get_music_info.py new file mode 100644 index 0000000..c7fdf3b --- /dev/null +++ b/examples/get_music_info.py @@ -0,0 +1,20 @@ +from client.api import TokApi + + +def example(music_id: str): + """ + Print hashtag info + """ + api = TokApi('YOUR_RAPID_API_KEY') + + result = api.get_music_info_by_id(music_id) + data = result.json() + + info = data['music_info'] + print('Music name: {}, user count: {}, listen url: {}'.format(info['title'], + info['user_count'], + info['play_url']['url_list'][0])) + + +if __name__ == "__main__": + example('6928004115846924290') diff --git a/examples/get_user_followers.py b/examples/get_user_followers.py new file mode 100644 index 0000000..511a9c9 --- /dev/null +++ b/examples/get_user_followers.py @@ -0,0 +1,25 @@ +from client.api import TokApi + + +def example(user_id: str): + """ + Print user followers info + """ + api = TokApi('YOUR_RAPID_API_KEY') + + max_time = 0 + for i in range(0, 3): + result = api.get_user_followers(user_id, offset=max_time) + data = result.json() + + for follower in data['followers']: + print('Nickname: {}, region: {}'.format(follower['nickname'], + follower['region'])) + if data['has_more']: + max_time = data['min_time'] + else: + break + + +if __name__ == "__main__": + example('6784563164518679557') diff --git a/examples/get_user_followings.py b/examples/get_user_followings.py new file mode 100644 index 0000000..eaa415e --- /dev/null +++ b/examples/get_user_followings.py @@ -0,0 +1,25 @@ +from client.api import TokApi + + +def example(user_id: str): + """ + Print user followings info + """ + api = TokApi('YOUR_RAPID_API_KEY') + + max_time = 0 + for i in range(0, 3): + result = api.get_user_followings(user_id, offset=max_time) + data = result.json() + + for follower in data['followings']: + print('Nickname: {}, region: {}'.format(follower['nickname'], + follower['region'])) + if data['has_more']: + max_time = data['min_time'] + else: + break + + +if __name__ == "__main__": + example('7018908209437377542') diff --git a/examples/get_user_info.py b/examples/get_user_info.py new file mode 100644 index 0000000..92538b2 --- /dev/null +++ b/examples/get_user_info.py @@ -0,0 +1,34 @@ +from client.api import TokApi + + +def example(user_id: str): + """ + Print user info + """ + api = TokApi('YOUR_RAPID_API_KEY') + + result = api.get_user_info_by_id(user_id) + data = result.json() + + info = data['user'] + print('Nickname: {}, video count: {}, followers: {}'.format(info['nickname'], + info['aweme_count'], + info['follower_count'])) + + +def example_qr_code(user_id: str): + """ + Print user info + """ + api = TokApi('YOUR_RAPID_API_KEY') + + result = api.get_user_qr_code(user_id) + data = result.json() + + print('QR code link: {}'.format(data['qrcode_url']['url_list'][1])) + + +if __name__ == "__main__": + example('6784563164518679557') + example('MS4wLjABAAAA0nDSqs_RbIwdxHKDTT3Il0MsEo1pvx4-fG6czO7rtP_3rWk1OzqViPk6ACM4hhv1') + example_qr_code('6784563164518679557') diff --git a/examples/get_video_info.py b/examples/get_video_info.py new file mode 100644 index 0000000..f185562 --- /dev/null +++ b/examples/get_video_info.py @@ -0,0 +1,40 @@ +from client.api import TokApi + + +def example_by_id(video_id: str): + """ + Print video info by id + """ + api = TokApi('YOUR_RAPID_API_KEY') + + result = api.get_video_info_by_id(video_id) + data = result.json() + + watch_url = data['aweme_detail']['video']['play_addr']['url_list'][0] + statistics = data['aweme_detail']['statistics'] + print('Likes: {}, views: {}, shares: {}, watch url: {}'.format(statistics['digg_count'], + statistics['play_count'], + statistics['share_count'], + watch_url)) + + +def example_by_link(video_web_link: str): + """ + Print video info by web link + """ + api = TokApi('YOUR_RAPID_API_KEY') + + result = api.get_video_info_by_web_link(video_web_link) + data = result.json() + + watch_url = data['aweme_detail']['video']['play_addr']['url_list'][0] + statistics = data['aweme_detail']['statistics'] + print('Likes: {}, views: {}, shares: {}, watch url: {}'.format(statistics['digg_count'], + statistics['play_count'], + statistics['share_count'], + watch_url)) + + +if __name__ == "__main__": + example_by_id('6977747303692078337') + example_by_link('https://www.tiktok.com/@adidas/video/7013447428070280454') diff --git a/examples/search_by_keyword.py b/examples/search_by_keyword.py new file mode 100644 index 0000000..5885138 --- /dev/null +++ b/examples/search_by_keyword.py @@ -0,0 +1,67 @@ +from client.api import TokApi + + +def search_user(keyword: str): + """ + Print results of user search + """ + api = TokApi('YOUR_RAPID_API_KEY') + + offset = 0 + for i in range(0, 3): + result = api.search_user_by_keyword(keyword, offset=offset) + data = result.json() + + for user in data['user_list']: + info = user['user_info'] + print('Nickname: {}, region: {}'.format(info['nickname'], + info['region'])) + + offset = data['cursor'] + + +def search_hashtag(keyword: str): + """ + Print results of hashtag search + """ + api = TokApi('YOUR_RAPID_API_KEY') + + offset = 0 + for i in range(0, 3): + result = api.search_hashtags_by_keyword(keyword, offset=offset) + data = result.json() + + for hashtag in data['challenge_list']: + info = hashtag['challenge_info'] + print('Name: {}, user count: {}'.format(info['cha_name'], + info['user_count'])) + + offset = data['cursor'] + + +def search_music(keyword: str): + """ + Print results of music search + """ + api = TokApi('YOUR_RAPID_API_KEY') + + offset = 0 + for i in range(0, 3): + result = api.search_music_by_keyword(keyword, offset=offset) + data = result.json() + + for music in data['music']: + print('Name: {}, user count: {}, listen url: {}'.format(music['title'], + music['user_count'], + music['play_url']['url_list'][0])) + + offset = data['cursor'] + + +if __name__ == "__main__": + print('==== User Search ====') + search_user('nike') + print('==== Hashtag Search ====') + search_hashtag('work') + print('==== Music Search ====') + search_music('happy new year') diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..3242f16 --- /dev/null +++ b/setup.py @@ -0,0 +1,24 @@ +import setuptools + +with open("README.md", "r") as fh: + long_description = fh.read() + +setuptools.setup( + name="tokapi-client", + version="0.0.2", + author="Somjik", + description="Simple http client for TokApi rapid-api tiktok mobile API", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/mtokapi/tokapi-client", + license="MIT", + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + python_requires='>=3.6', + packages=["client"], + include_package_data=True, + install_requires=['httpx'] +)