├── LICENSE ├── README.md ├── composer.json └── lib └── TikTokPrivate ├── Api.php ├── Transform.php └── Util.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sovit Tamrakar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unofficial TikTok Private API library for PHP 2 | API Wrapper for private API access 3 | 4 | # Installation 5 | Via composer `composer require ssovit/tiktok-private-api` 6 | 7 | 8 | # How does this work? 9 | Monthly subscription of my private API server is required for this to function. 10 | See below. 11 | 12 | 13 | # Documentation 14 | 15 | https://ssovit.github.io/TikTok-Private-API-PHP/ 16 | 17 | # Usage 18 | Follow examples in `/example` directory 19 | 20 | ```php 21 | $api=new \Sovit\TikTokPrivate\Api(array(/* config array*/)); 22 | 23 | $trendingFeed=$api->getForYou($maxCursor=0); 24 | 25 | $userData=$api->getUser("USERNAME"); 26 | 27 | $userFeed=$api->getUserFeed("USER_ID",$maxCursor=0); 28 | 29 | $challenge=$api->getChallenge("CHALLENGE_ID"); 30 | 31 | $challengeFeed=$api->getChallengeFeed("CHALLENGE_ID",$maxCursor=0); 32 | 33 | $musc=$api->getMusic("6798898508385585925"); 34 | 35 | $musicFeed=$api->getMusicFeed("6798898508385585925",$maxCursor=0); 36 | 37 | $videoData=$api->getVideoByID("6829540826570296577"); 38 | 39 | $videoData=$api->getVideoByUrl("https://www.tiktok.com/@zachking/video/6829303572832750853"); 40 | 41 | // More to come 42 | 43 | ``` 44 | 45 | # Available Options 46 | ```php 47 | $api=new \Sovit\TikTokPrivate\Api(array( 48 | "proxy" => '', // proxy in url format like http://username:password@host:port 49 | "cache_timeout" => 3600 // 1 hours cache timeout 50 | "transform_result" => true, // false if you want to get json without transforming it to more readable JSON structure 51 | "api_key" => "API_KEY" // see below on how to get API key 52 | ), $cache_engine=false); 53 | ``` 54 | 55 | # Cache Engine 56 | You can build your own engine that will store and fetch cache from your local storage to prevent frequent requests to TikTok server. This can help being banned from TikTok server for too frequent requests. 57 | 58 | Cache engine should have callable `get` and `set` methods that the API class uses 59 | ```php 60 | // Example using WordPress transient as cache engine 61 | Class MyCacheEngine{ 62 | function get($cache_key){ 63 | return get_transient($cache_key); 64 | } 65 | function set($cache_key,$data,$timeout=3600){ 66 | return set_transient($cache_key,$data,$timeout); 67 | } 68 | } 69 | 70 | ``` 71 | **Usage** 72 | ```php 73 | $cache_engine=new MyCacheEngine(); 74 | $api=new \Sovit\TikTokPrivate\Api(array(/* config array*/),$cache_engine); 75 | ``` 76 | 77 | # Available methods 78 | - `getForYou` - Get trending feed `getForYou($maxCursor)` 79 | - `getUser` - Get profile data for TikTok User `getUser($username)` 80 | - `getUserFeed` - Get user feed by ID `getUserFeed($user_id,$maxCursor)` 81 | - `getChallenge` - Get challenge/hashtag info `getChallenge($challenge)` 82 | - `getChallengeFeed` - Get challenge feed by ID `getChallengeFeed($challenge_id, $maxCursor)` 83 | - `getMusic` - Get music info `getMusic($music_id)` 84 | - `getMusicFeed` - Get music feed `getMusicFeed($music_id,$maxCursor)` 85 | - `getVideoByID` - Get video by ID `getVideoByID($video_id)` 86 | - `getVideoByUrl` - Get video by URL `getVideoByUrl($video_url)` 87 | - *not all methods are documented, will update when I am free* 88 | `$maxCursor` defaults to `0`, and is offset for results page. `maxCursor` for next page is exposed on current page call feed data. 89 | 90 | 91 | ## Pirvate API server subscription pricing 92 | *No Longer Available* 93 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssovit/tiktok-private-api", 3 | "description": "Unofficial TikTok Private API for PHP", 4 | "license": "MIT", 5 | "homepage": "https://github.com/ssovit/tiktok-private-api", 6 | "keywords": [ 7 | "tiktok", 8 | "tiktok-api", 9 | "tiktok-app-api", 10 | "tiktok-scraper" 11 | ], 12 | "authors": [ 13 | { 14 | "name": "Sovit Tamrakar", 15 | "email": "sovit.tamrakar@gmail.com", 16 | "homepage": "https://github.com/ssovit/", 17 | "role": "Developer" 18 | } 19 | ], 20 | "support": { 21 | "source": "https://github.com/ssovit/tiktok-private-api", 22 | "issues": "https://github.com/ssovit/tiktok-private-api/issues", 23 | "email": "sovit.tamrakar@gmail.com" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Sovit\\": "lib" 28 | } 29 | }, 30 | "require": { 31 | "guzzlehttp/guzzle": "^6.5 || ^7.0" 32 | }, 33 | "type": "library", 34 | "archive": { 35 | "exclude": [ 36 | "/example", 37 | "/docs", 38 | "/phpdoc.dist.xml", 39 | "/.gitattributes", 40 | "/.gitignore", 41 | "/package.json", 42 | "/.github" 43 | ] 44 | } 45 | } -------------------------------------------------------------------------------- /lib/TikTokPrivate/Api.php: -------------------------------------------------------------------------------- 1 | null, 53 | "cache_timeout" => 3600, 54 | "transform_result" => false, // transform not recommended as tiktok json structure is always changing and can be broken at any time. 55 | ]; 56 | 57 | /** 58 | * Class Constructor 59 | * 60 | * @param array $config API Config 61 | * @param object|false $cacheEngine 62 | * @return void 63 | */ 64 | public function __construct($config = [], $cacheEngine = false) 65 | { 66 | /** 67 | * Initialize the config array 68 | */ 69 | $this->_config = array_merge([], $this->defaults, $config); 70 | 71 | /** 72 | * If Cache Engine is enabled 73 | */ 74 | if ($cacheEngine) { 75 | $this->cacheEnabled = true; 76 | $this->cacheEngine = $cacheEngine; 77 | } 78 | } 79 | 80 | /** 81 | * Get Challenge detail 82 | * 83 | * @param string $challenge_id Challenge ID 84 | * @return object|false Returns object or false on failure 85 | */ 86 | public function getChallenge($challenge_id) 87 | { 88 | /** 89 | * Check if challenge is not empty 90 | */ 91 | if (empty($challenge_id)) { 92 | throw new \Exception("Invalid Challenge"); 93 | } 94 | $cacheKey = 'challenge-' . $challenge_id; 95 | if ($this->cacheEnabled) { 96 | if ($this->cacheEngine->get($cacheKey)) { 97 | return $this->cacheEngine->get($cacheKey); 98 | } 99 | } 100 | $result = $this->remote_call("challenge/" . $challenge_id); 101 | if (isset($result->ch_info)) { 102 | if (true === $this->_config['transform_result']) { 103 | $result = Transform::Challenge($result->ch_info); 104 | } 105 | if ($this->cacheEnabled) { 106 | $this->cacheEngine->set($cacheKey, $result, $this->_config['cache_timeout']); 107 | } 108 | return $result; 109 | } 110 | return $this->failure(); 111 | } 112 | 113 | /** 114 | * Get Challenge Feed 115 | * 116 | * @param string $challenge_id Challenge ID 117 | * @param integer $cursor Offset Cursor 118 | * @return object|false Returns object or false on failure 119 | */ 120 | public function getChallengeFeed($challenge_id, $cursor = 0) 121 | { 122 | if (empty($challenge_id)) { 123 | throw new \Exception("Invalid Challenge"); 124 | } 125 | $cacheKey = 'challenge-' . $challenge_id . '-' . $cursor; 126 | if ($this->cacheEnabled) { 127 | if ($this->cacheEngine->get($cacheKey)) { 128 | return $this->cacheEngine->get($cacheKey); 129 | } 130 | } 131 | $result = $this->remote_call("challenge/" . $challenge_id . "/feed", [ 132 | 'maxCursor' => $cursor, 133 | ]); 134 | if (isset($result->aweme_list)) { 135 | if (true === $this->_config['transform_result']) { 136 | $result = Transform::Feed($result); 137 | } 138 | if ($this->cacheEnabled) { 139 | $this->cacheEngine->set($cacheKey, $result, $this->_config['cache_timeout']); 140 | } 141 | return $result; 142 | } 143 | return $this->failure(); 144 | } 145 | 146 | /** 147 | * Get Challenge ID By Challenge name 148 | * 149 | * @param string $challenge_name Challenge Name 150 | * @return string|false Returns challenge ID or false on failure 151 | */ 152 | public function getChallengeID($challenge_name) 153 | { 154 | if (empty($challenge_name)) { 155 | throw new \Exception("Invalid Challenge"); 156 | } 157 | $cacheKey = 'chid-' . $challenge_name; 158 | if ($this->cacheEnabled) { 159 | if ($this->cacheEngine->get($cacheKey)) { 160 | return $this->cacheEngine->get($cacheKey); 161 | } 162 | } 163 | $result = $this->searchChallenge($challenge_name); 164 | if ($result) { 165 | $result = Util::find($result->challenge_list, function ($item) use ($challenge_name) { 166 | return $item->challenge_info->cha_name === $challenge_name; 167 | }); 168 | if ($result) { 169 | if ($this->cacheEnabled) { 170 | $this->cacheEngine->set($cacheKey, $result->challenge_info->cid, 86400 * 365); 171 | } 172 | return $result->challenge_info->cid; 173 | } 174 | } 175 | return $this->failure(); 176 | } 177 | 178 | /** 179 | * Get video comments 180 | * 181 | * @param string $video_id Video ID 182 | * @param integer $cursor Offset Cursor 183 | * @return object|false Returns object or false on failure 184 | */ 185 | public function getComments($video_id, $cursor = 0) 186 | { 187 | if (empty($video_id)) { 188 | throw new \Exception("Invalid Video ID"); 189 | } 190 | $cacheKey = 'comments-' . $video_id; 191 | if ($this->cacheEnabled) { 192 | if ($this->cacheEngine->get($cacheKey)) { 193 | return $this->cacheEngine->get($cacheKey); 194 | } 195 | } 196 | $result = $this->remote_call("comments/" . $video_id, [ 197 | 'maxCursor' => $cursor, 198 | ]); 199 | if (isset($result->comments)) { 200 | if ($this->cacheEnabled) { 201 | $this->cacheEngine->set($cacheKey, $result, $this->_config['cache_timeout']); 202 | } 203 | return $result; 204 | } 205 | return $this->failure(); 206 | } 207 | 208 | /** 209 | * Trending Feed 210 | * 211 | * @return object|false Returns object or false on failure 212 | */ 213 | public function getForYou($cursor = 0) 214 | { 215 | $cacheKey = 'trending-' . $cursor; 216 | if ($this->cacheEnabled) { 217 | if ($this->cacheEngine->get($cacheKey)) { 218 | return $this->cacheEngine->get($cacheKey); 219 | } 220 | } 221 | $result = $this->remote_call("trending"); 222 | if (isset($result->aweme_list)) { 223 | if (true === $this->_config['transform_result']) { 224 | $result = Transform::Feed($result); 225 | $result->maxCursor = ++$cursor; 226 | } 227 | if ($this->cacheEnabled) { 228 | $this->cacheEngine->set($cacheKey, $result, $this->_config['cache_timeout']); 229 | } 230 | return $result; 231 | } 232 | return $this->failure(); 233 | } 234 | 235 | /** 236 | * Get Music detail 237 | * 238 | * @param string $music_id Music ID 239 | * @return object|false Returns object or false on failure 240 | */ 241 | public function getMusic($music_id) 242 | { 243 | if (empty($music_id)) { 244 | throw new \Exception("Invalid Music ID"); 245 | } 246 | $cacheKey = 'music-' . $music_id; 247 | if ($this->cacheEnabled) { 248 | if ($this->cacheEngine->get($cacheKey)) { 249 | return $this->cacheEngine->get($cacheKey); 250 | } 251 | } 252 | $result = $this->remote_call("music/" . $music_id); 253 | if (isset($result->music_info)) { 254 | if (true === $this->_config['transform_result']) { 255 | $result = Transform::Music($result->music_info); 256 | } 257 | if ($this->cacheEnabled) { 258 | $this->cacheEngine->set($cacheKey, $result, $this->_config['cache_timeout']); 259 | } 260 | return $result; 261 | } 262 | return $this->failure(); 263 | } 264 | 265 | /** 266 | * Get music feed 267 | * 268 | * @param string $music_id Music ID 269 | * @param int $cursor Offset Cursor 270 | * @return object|false Returns object or false on failure 271 | */ 272 | public function getMusicFeed($music_id, $cursor = 0) 273 | { 274 | if (empty($music_id)) { 275 | throw new \Exception("Invalid Music ID"); 276 | } 277 | $cacheKey = 'music-feed-' . $music_id . '-' . $cursor; 278 | if ($this->cacheEnabled) { 279 | if ($this->cacheEngine->get($cacheKey)) { 280 | return $this->cacheEngine->get($cacheKey); 281 | } 282 | } 283 | $result = $this->remote_call("music/{$music_id}/feed", [ 284 | "maxCursor" => $cursor, 285 | ]); 286 | if (isset($result->aweme_list)) { 287 | if (true === $this->_config['transform_result']) { 288 | $result = Transform::Feed($result); 289 | } 290 | if ($this->cacheEnabled) { 291 | $this->cacheEngine->set($cacheKey, $result, $this->_config['cache_timeout']); 292 | } 293 | return $result; 294 | } 295 | return $this->failure(); 296 | } 297 | /** 298 | * Get user detail by username 299 | * 300 | * @param string $username 301 | * @return object|false Returns object or false on failure 302 | */ 303 | function getUser($username) 304 | { 305 | return $this->getUserFromSearch($username); 306 | } 307 | 308 | /** 309 | * Get User detail by ID 310 | * 311 | * @param string $user_id User ID 312 | * @return object|false Returns object or false on failure 313 | */ 314 | public function getUserByID($user_id) 315 | { 316 | if (empty($user_id)) { 317 | throw new \Exception("Invalid Username"); 318 | } 319 | $cacheKey = 'user-' . $user_id; 320 | if ($this->cacheEnabled) { 321 | if ($this->cacheEngine->get($cacheKey)) { 322 | return $this->cacheEngine->get($cacheKey); 323 | } 324 | } 325 | $result = $this->remote_call("user/{$user_id}"); 326 | if (isset($result->user)) { 327 | if (true === $this->_config['transform_result']) { 328 | $result = Transform::User($result->user); 329 | } 330 | if ($this->cacheEnabled) { 331 | $this->cacheEngine->set($cacheKey, $result, $this->_config['cache_timeout']); 332 | } 333 | return $result; 334 | } 335 | return $this->failure(); 336 | } 337 | /** 338 | * Get user detail from search by username as keyword 339 | * 340 | * @param string $username 341 | * @return object|false Returns object or false on failure 342 | */ 343 | public function getUserFromSearch($username) 344 | { 345 | if (empty($username)) { 346 | throw new \Exception("Invalid Username"); 347 | } 348 | $search = $this->searchUser($username); 349 | if ($search !== false && !empty($search->data) && isset($search->data[0]->user_list)) { 350 | $result = Util::find($search->data[0]->user_list, function ($item) use ($username) { 351 | return $item->user_info->unique_id === $username; 352 | }); 353 | if ($result) { 354 | $result = $result->user_info; 355 | if (true === $this->_config['transform_result']) { 356 | $result = Transform::User($result); 357 | } 358 | return $result; 359 | } 360 | return $this->failure(); 361 | } 362 | } 363 | 364 | /** 365 | * Get user feed 366 | * 367 | * @param string $user_id User ID 368 | * @param integer $cursor Offset Cursor 369 | * @return object|false Returns object or false on failure 370 | */ 371 | public function getUserFeed($user_id, $cursor = 0) 372 | { 373 | if (empty($user_id)) { 374 | throw new \Exception("Invalid Username"); 375 | } 376 | $cacheKey = 'user-feed-' . $user_id . '-' . $cursor; 377 | if ($this->cacheEnabled) { 378 | if ($this->cacheEngine->get($cacheKey)) { 379 | return $this->cacheEngine->get($cacheKey); 380 | } 381 | } 382 | $result = $this->remote_call("user/{$user_id}/feed", [ 383 | "maxCursor" => $cursor, 384 | ]); 385 | if (isset($result->aweme_list)) { 386 | if (true === $this->_config['transform_result']) { 387 | $result = Transform::Feed($result); 388 | } 389 | if ($this->cacheEnabled) { 390 | $this->cacheEngine->set($cacheKey, $result, $this->_config['cache_timeout']); 391 | } 392 | return $result; 393 | } 394 | return $this->failure(); 395 | } 396 | 397 | /** 398 | * Get User Followers 399 | * 400 | * @param string $user_id User ID 401 | * @param integer $cursor Offset Cursor 402 | * @return object|false Returns object or false on failure 403 | */ 404 | public function getUserFollowers($user_id, $cursor = 0) 405 | { 406 | if (empty($user_id)) { 407 | throw new \Exception("Invalid User ID"); 408 | } 409 | $cacheKey = 'follower-' . $user_id . '-' . $cursor; 410 | if ($this->cacheEnabled) { 411 | if ($this->cacheEngine->get($cacheKey)) { 412 | return $this->cacheEngine->get($cacheKey); 413 | } 414 | } 415 | 416 | $result = $this->remote_call("followers/{$user_id}", [ 417 | "maxCursor" => $cursor, 418 | 419 | ]); 420 | if (isset($result->followers)) { 421 | if ($this->cacheEnabled) { 422 | $this->cacheEngine->set($cacheKey, $result, $this->_config['cache_timeout']); 423 | } 424 | return $result; 425 | } 426 | return $this->failure(); 427 | } 428 | 429 | /** 430 | * Get User Followings 431 | * 432 | * @param string $user_id User ID 433 | * @param integer $cursor Offset Cursor 434 | * @return object|false Returns object or false on failure 435 | */ 436 | public function getUserFollowings($user_id, $cursor = 0) 437 | { 438 | if (empty($user_id)) { 439 | throw new \Exception("Invalid User ID"); 440 | } 441 | $cacheKey = 'follower-' . $user_id . '-' . $cursor; 442 | if ($this->cacheEnabled) { 443 | if ($this->cacheEngine->get($cacheKey)) { 444 | return $this->cacheEngine->get($cacheKey); 445 | } 446 | } 447 | 448 | $result = $this->remote_call("following/{$user_id}", [ 449 | "maxCursor" => $cursor, 450 | 451 | ]); 452 | if (isset($result->followings)) { 453 | if ($this->cacheEnabled) { 454 | $this->cacheEngine->set($cacheKey, $result, $this->_config['cache_timeout']); 455 | } 456 | return $result; 457 | } 458 | return $this->failure(); 459 | } 460 | 461 | /** 462 | * Get user id by username 463 | * 464 | * @param string $username Username 465 | * @return string|false Returns user ID or false on failure 466 | */ 467 | public function getUserID($username) 468 | { 469 | if (empty($username)) { 470 | throw new \Exception("Invalid User"); 471 | } 472 | $cacheKey = 'userid-' . $username; 473 | if ($this->cacheEnabled) { 474 | if ($this->cacheEngine->get($cacheKey)) { 475 | return $this->cacheEngine->get($cacheKey); 476 | } 477 | } 478 | $result = $this->remote_call("suggestion/{$username}"); 479 | if ($result) { 480 | $result = Util::find($result->sug_list, function ($item) use ($username) { 481 | return $item->extra_info->sug_uniq_id === $username; 482 | }); 483 | if ($result) { 484 | if ($this->cacheEnabled) { 485 | $this->cacheEngine->set($cacheKey, $result->extra_info->sug_user_id, 86400 * 365); 486 | } 487 | return $result->extra_info->sug_user_id; 488 | } 489 | } 490 | return $this->failure(); 491 | } 492 | 493 | /** 494 | * Get video by video id 495 | * 496 | * @param string $video_id Video ID 497 | * @return object|false Returns object or false on failure 498 | */ 499 | public function getVideoByID($video_id) 500 | { 501 | if (empty($video_id)) { 502 | throw new \Exception("Invalid Video ID"); 503 | } 504 | $cacheKey = 'video-' . $video_id; 505 | if ($this->cacheEnabled) { 506 | if ($this->cacheEngine->get($cacheKey)) { 507 | return $this->cacheEngine->get($cacheKey); 508 | } 509 | } 510 | $result = $this->remote_call("video/{$video_id}"); 511 | if (isset($result->aweme_detail)) { 512 | if (true === $this->_config['transform_result']) { 513 | $result = Transform::Item($result->aweme_detail); 514 | } 515 | if ($this->cacheEnabled) { 516 | $this->cacheEngine->set($cacheKey, $result, $this->_config['cache_timeout']); 517 | } 518 | return $result; 519 | } 520 | return $this->failure(); 521 | } 522 | 523 | /** 524 | * Get Video by TikTok URL 525 | * 526 | * @param string $url Video URL 527 | * @return object|false Returns object or false on failure 528 | */ 529 | public function getVideoByUrl($url) 530 | { 531 | if (!preg_match("/https?:\/\/([^\.]+)?\.tiktok\.com/", $url)) { 532 | throw new \Exception("Invalid Video URL"); 533 | } 534 | if (!preg_match("/(video|v)\/([\d]+)/", $url)) { 535 | $url = $this->finalUrl($url); 536 | } 537 | if (preg_match("/(video|v)\/([\d]+)/", $url, $match)) { 538 | return $this->getVideoByID($match[2]); 539 | } 540 | return $this->failure(); 541 | } 542 | 543 | /** 544 | * Search challenge by challenge name 545 | * 546 | * @param string $keyword Search Keyword 547 | * @param integer $cursor Offset Cursor 548 | * @return object|false Returns object or false on failure 549 | */ 550 | public function searchChallenge($keyword, $cursor = 0) 551 | { 552 | if (empty($keyword)) { 553 | throw new \Exception("Invalid keyword"); 554 | } 555 | $cacheKey = 'search-challenge-' . $keyword . '-' . $cursor; 556 | if ($this->cacheEnabled) { 557 | if ($this->cacheEngine->get($cacheKey)) { 558 | return $this->cacheEngine->get($cacheKey); 559 | } 560 | } 561 | $result = $this->remote_call("search/challenge/" . $keyword, [ 562 | 'maxCursor' => $cursor, 563 | ]); 564 | if (isset($result->challenge_list)) { 565 | if ($this->cacheEnabled) { 566 | $this->cacheEngine->set($cacheKey, $result, $this->_config['cache_timeout']); 567 | } 568 | return $result; 569 | } 570 | return $this->failure(); 571 | } 572 | 573 | /** 574 | * Search User by username 575 | * 576 | * @param string $keyword Serch Keyword 577 | * @param integer $cursor Offset Cursor 578 | * @return object|false Returns object or false on failure 579 | */ 580 | public function searchUser($keyword, $cursor = 0) 581 | { 582 | if (empty($keyword)) { 583 | throw new \Exception("Invalid keyword"); 584 | } 585 | $cacheKey = 'search-user-' . $keyword . '-' . $cursor; 586 | if ($this->cacheEnabled) { 587 | if ($this->cacheEngine->get($cacheKey)) { 588 | return $this->cacheEngine->get($cacheKey); 589 | } 590 | } 591 | $result = $this->remote_call("search/user/" . $keyword, [ 592 | 'maxCursor' => $cursor, 593 | ]); 594 | if (isset($result->data[0]->user_list)) { 595 | if ($this->cacheEnabled) { 596 | $this->cacheEngine->set($cacheKey, $result, $this->_config['cache_timeout']); 597 | } 598 | return $result; 599 | } 600 | return $this->failure(); 601 | } 602 | 603 | /** 604 | * Check if api key is provided 605 | * 606 | * @return void 607 | */ 608 | private function checkAPIKey() 609 | { 610 | if (empty($this->_config['api_key'])) { 611 | throw new \Exception("Valid API Key is required"); 612 | } 613 | } 614 | 615 | /** 616 | * Failure 617 | * 618 | * Be a man and accept the failure. 619 | * 620 | * 621 | * @return false Returns false 622 | */ 623 | private function failure() 624 | { 625 | //\error_log("Something went wrong"); 626 | return false; 627 | } 628 | 629 | /** 630 | * Get final redirect URL 631 | * 632 | * @param string $url Video Post URL 633 | * @return string Returns final redirect url 634 | */ 635 | private function finalUrl($url) 636 | { 637 | try { 638 | $client = new Client(); 639 | 640 | $result = $client->get($url, [ 641 | "headers"=>["user-agent"=>"okhttp"], 642 | "allow_redirects"=>["max"=>1], 643 | 'on_stats' => function (TransferStats $stats) use (&$url) { 644 | $url = $stats->getEffectiveUri(); 645 | }, 646 | 'verify' => false, 647 | "proxy" => $this->_config['proxy'], 648 | ]); 649 | return $url; 650 | } catch (\Exception $e) { 651 | return $url; 652 | } 653 | } 654 | 655 | /** 656 | * Make remote call 657 | * 658 | * @param string $path Remote path 659 | * @param array $params Parameters 660 | * @return object|false Returns object or false on failure 661 | */ 662 | private function remote_call($path, $params = []) 663 | { 664 | $this->checkAPIKey(); 665 | $params['key'] = $this->_config['api_key']; 666 | try { 667 | $client = new Client(); 668 | 669 | $response = $client->get(trim($this->api_base, "/") . "/" . trim($path, "/"), [ 670 | "query" => $params, 671 | 'verify' => false, 672 | ]); 673 | $result = json_decode($response->getBody(), true); 674 | $response = $client->get($result['url'], [ 675 | "headers" => $result['headers'], 676 | "proxy" => $this->_config['proxy'], 677 | 'verify' => false, 678 | ]); 679 | $result = json_decode($response->getBody(), false); 680 | 681 | return $result; 682 | } catch (\Exception $e) { 683 | return false; 684 | } 685 | return false; 686 | } 687 | } 688 | } 689 | -------------------------------------------------------------------------------- /lib/TikTokPrivate/Transform.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'id' => @$challenge->cid, 17 | 'title' => @$challenge->cha_name, 18 | 'desc' => @$challenge->desc, 19 | 'profileLarger' => @$challenge->hashtag_profile, 20 | 'profileMedium' => @$challenge->hashtag_profile, 21 | 'profileThumb' => @$challenge->hashtag_profile, 22 | 'coverLarger' => @$challenge->cover_photo, 23 | 'coverMedium' => @$challenge->cover_photo, 24 | 'coverThumb' => @$challenge->cover_photo, 25 | 'isCommerce' => @$challenge->is_commerce, 26 | ], 27 | 'stats' => [ 28 | 'videoCount' => @$challenge->user_count, 29 | 'viewCount' => @$challenge->view_count, 30 | ], 31 | ]; 32 | return (object) $result; 33 | } 34 | 35 | /** 36 | * Transform Feed data 37 | * 38 | * @param object $data 39 | * @return object 40 | */ 41 | public static function Feed($data) { 42 | $result = [ 43 | 'statusCode' => 0, 44 | 'hasMore' => true == $data->has_more, 45 | 'maxCursor' => isset($data->max_cursor) ? $data->max_cursor : (isset($data->cursor) ? $data->cursor : 0), 46 | 'items' => self::Items($data->aweme_list), 47 | ]; 48 | return (object) $result; 49 | } 50 | 51 | public static function Item($item) { 52 | $result = [ 53 | 'id' => @$item->aweme_id, 54 | 'desc' => @$item->desc, 55 | 'createTime' => @$item->create_time, 56 | 'video' => [ 57 | 'id' => @$item->video->play_addr->uri, 58 | 'height' => @$item->video->height, 59 | 'width' => @$item->video->width, 60 | 'duration' => @$item->video->duration, 61 | 'ratio' => @$item->video->ratio, 62 | 'cover' => @$item->video->cover->url_list[0], 63 | 'originCover' => @$item->video->origin_cover->url_list[0], 64 | 'dynamicCover' => @$item->video->animated_cover->url_list[0], 65 | 'playAddr' => @$item->video->download_addr->url_list[0], 66 | 'downloadAddr' => @$item->video->download_addr->url_list[0], 67 | 'noWatermarkAddr' => @$item->video->play_addr->url_list[0], 68 | 69 | ], 70 | 'author' => [ 71 | 'id' => @$item->author->uid, 72 | 'shortId' => @$item->author->short_id, 73 | 'uniqueId' => @$item->author->unique_id, 74 | 'nickname' => @$item->author->nickname, 75 | 'avatarLarger' => @$item->author->avatar_larger->url_list[0], 76 | 'avatarMedium' => @$item->author->avatar_medium->url_list[0], 77 | 'avatarThumb' => @$item->author->avatar_thumb->url_list[0], 78 | 'verified' => @!empty($item->author->custom_verify), 79 | 'secUid' => @$item->author->sec_uid, 80 | 'commentSetting' => @$item->author->comment_setting, 81 | 'duetSetting' => @$item->author->duet_setting, 82 | 'stitchSetting' => @$item->author->stitch_setting, 83 | 'privateAccount' => @$item->author->secret, 84 | 'secret' => @$item->author->secret, 85 | 'roomId' => @$item->author->room_id, 86 | ], 87 | 'music' => [ 88 | 'id' => @$item->music->mid, 89 | 'title' => @$item->music->title, 90 | 'playUrl' => @$item->music->play_url->url_list[0], 91 | 'coverLarge' => @$item->music->cover_large->url_list[0], 92 | 'coverMedium' => @$item->music->cover_medium->url_list[0], 93 | 'coverThumb' => @$item->music->cover_thumb->url_list[0], 94 | 'authorName' => @$item->music->author, 95 | 'original' => @$item->music->is_original, 96 | 'duration' => @$item->music->duration, 97 | 'album' => @$item->music->album, 98 | ], 99 | 'stats' => [ 100 | 'diggCount' => @$item->statistics->digg_count, 101 | 'shareCount' => @$item->statistics->share_count, 102 | 'commentCount' => @$item->statistics->comment_count, 103 | 'playCount' => @$item->statistics->play_count, 104 | 'downloadCount' => @$item->statistics->download_count, 105 | 'whatsAppShareCount' => @$item->statistics->whatsapp_share_count, 106 | 'forwardCount' => @$item->statistics->forward_count, 107 | ], 108 | 'region' => @$item->region, 109 | 'secret' => false, 110 | 'privateItem' => false, 111 | 'duetEnabled' => @$item->video_control->allow_duet, 112 | 'stitchEnabled' => @$item->video_control->allow_stitch, 113 | 'shareEnabled' => @$item->status->allow_share, 114 | 'reactEnabled' => @$item->status->allow_react, 115 | 'isAd' => @$item->is_ads, 116 | ]; 117 | return (object) $result; 118 | } 119 | 120 | /** 121 | * Process Feed Items 122 | * 123 | * @param object $items 124 | * @return object 125 | */ 126 | public static function Items($items) { 127 | $result = []; 128 | foreach ($items as $item) { 129 | $result[] = self::Item($item); 130 | } 131 | return (object) $result; 132 | } 133 | 134 | /** 135 | * Transform Music Data 136 | * 137 | * @param object $data 138 | * @return object 139 | */ 140 | public static function Music($music) { 141 | $result = [ 142 | 'music' => [ 143 | 'id' => @$music->mid, 144 | 'title' => @$music->title, 145 | 'playUrl' => @$music->play_url->url_list[0], 146 | 'coverLarge' => @$music->cover_large->url_list[0], 147 | 'coverMedium' => @$music->cover_medium->url_list[0], 148 | 'coverThumb' => @$music->cover_thumb->url_list[0], 149 | 'authorName' => @$music->author, 150 | 'original' => @$music->is_original, 151 | 'duration' => @$music->duration, 152 | 'album' => @$music->album, 153 | ], 154 | 'stats' => [ 155 | 'videoCount' => @$music->user_count, 156 | ], 157 | ]; 158 | return (object) $result; 159 | } 160 | 161 | /** 162 | * Transform User data 163 | * 164 | * @param object $data 165 | * @return object 166 | */ 167 | public static function User($user) { 168 | $result = [ 169 | 'user' => [ 170 | 'id' => @$user->uid, 171 | 'shortId' => @$user->short_id, 172 | 'uniqueId' => @$user->unique_id, 173 | 'nickname' => @$user->nickname, 174 | 'avatarLarger' => @$user->avatar_larger->url_list[0], 175 | 'avatarMedium' => @$user->avatar_medium->url_list[0], 176 | 'avatarThumb' => @$user->avatar_thumb->url_list[0], 177 | 'signature' => @$user->signature, 178 | 'verified' => !empty($user->custom_verify), 179 | 'secUid' => @$user->sec_uid, 180 | 'openFavorite' => @true == $user->show_favorite_list, 181 | "bioLink" => @isset($user->bio_url) ? [ 182 | "link" => @$user->bio_url, 183 | "risk" => 0, 184 | ] : null, 185 | "bioEmail" => @$user->bio_email, 186 | "category" => @$user->category, 187 | "twitter" => @$user->twitter_id, 188 | "youtubeChannelId" => @$user->youtube_channel_id, 189 | "instagramId" => @$user->ins_id, 190 | 'commentSetting' => @true == $user->comment_setting, 191 | 'duetSetting' => @true == $user->duet_setting, 192 | 'privateAccount' => @true == $user->secret, 193 | 'secret' => @true == $user->secret, 194 | 'isADVirtual' => @true == $user->ad_virtual, 195 | 'roomId' => @$user->room_id, 196 | ], 197 | 'stats' => [ 198 | 'followerCount' => @$user->follower_count, 199 | 'followingCount' => @$user->following_count, 200 | 'heart' => @$user->total_favorited, 201 | 'heartCount' => @$user->total_favorited, 202 | 'videoCount' => @$user->aweme_count, 203 | 'diggCount' => @$user->favoriting_count, 204 | ], 205 | ]; 206 | return (object) $result; 207 | } 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /lib/TikTokPrivate/Util.php: -------------------------------------------------------------------------------- 1 |