diff --git a/README.md b/README.md index 35cc551c..5e057cbc 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,22 @@ $instagram->login(); // will use cached session if you can force login $instagra $account = $instagram->getAccountById(3); echo $account->getUsername(); ``` +Using proxy for requests: + +```php +$instagram = new Instagram(); +Instagram::setProxy([ + 'address' => '111.112.113.114', + 'port' => '8080', + 'tunnel' => true, + 'timeout' => 30, +]); +// Request with proxy +$account = $instagram->getAccount('kevin'); +Instagram::disableProxy(); +// Request without proxy +$account = $instagram->getAccount('kevin'); +``` ## Installation diff --git a/examples/getAccountById.php b/examples/getAccountById.php index 34a294a3..ca8fd1d2 100644 --- a/examples/getAccountById.php +++ b/examples/getAccountById.php @@ -14,7 +14,7 @@ echo "Profile picture url: {$account->getProfilePicUrl()}\n"; echo "External link: {$account->getExternalUrl()}\n"; echo "Number of published posts: {$account->getMediaCount()}\n"; -echo "Number of followers: {$account->getFollowsCount()}\n"; -echo "Number of follows: {$account->getFollowedByCount()}\n"; +echo "Number of followers: {$account->getFollowedByCount()}\n"; +echo "Number of follows: {$account->getFollowsCount()}\n"; echo "Is private: {$account->isPrivate()}\n"; echo "Is verified: {$account->isVerified()}\n"; diff --git a/examples/getAccountFollowings.php b/examples/getAccountFollowings.php new file mode 100644 index 00000000..80adb6a2 --- /dev/null +++ b/examples/getAccountFollowings.php @@ -0,0 +1,13 @@ +login(); +sleep(2); // Delay to mimic user + +$username = 'kevin'; +$followers = []; +$account = $instagram->getAccount($username); +sleep(1); +$followers = $instagram->getFollowing($account->getId(), 1000, 100, true); // Get 1000 followings of 'kevin', 100 a time with random delay between requests +echo '
' . json_encode($followers, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . '
'; \ No newline at end of file diff --git a/examples/getSidecarMediaByUrl.php b/examples/getSidecarMediaByUrl.php new file mode 100644 index 00000000..45318360 --- /dev/null +++ b/examples/getSidecarMediaByUrl.php @@ -0,0 +1,39 @@ +getId()}\n"; + echo "${padding}Shotrcode: {$media->getShortCode()}\n"; + echo "${padding}Created at: {$media->getCreatedTime()}\n"; + echo "${padding}Caption: {$media->getCaption()}\n"; + echo "${padding}Number of comments: {$media->getCommentsCount()}\n"; + echo "${padding}Number of likes: {$media->getLikesCount()}\n"; + echo "${padding}Get link: {$media->getLink()}\n"; + echo "${padding}High resolution image: {$media->getImageHighResolutionUrl()}\n"; + echo "${padding}Media type (video/image/sidecar): {$media->getType()}\n"; +} + +// If account is public you can query Instagram without auth +$instagram = new \InstagramScraper\Instagram(); + +// If account is private and you subscribed to it firstly login +$instagram = \InstagramScraper\Instagram::withCredentials('username', 'password', '/path/to/cache/folder'); +$instagram->login(); + +$media = $instagram->getMediaByUrl('https://www.instagram.com/p/BQ0lhTeAYo5'); +echo "Media info:\n"; +printMediaInfo($media); + +$padding = ' '; +echo "Sidecar medias info:\n"; +foreach ($media->getSidecarMedias() as $sidecarMedia) { + printMediaInfo($sidecarMedia, $padding); + echo "\n"; +} + +$account = $media->getOwner(); +echo "Account info:\n"; +echo "Id: {$account->getId()}\n"; +echo "Username: {$account->getUsername()}\n"; +echo "Full name: {$account->getFullName()}\n"; +echo "Profile pic url: {$account->getProfilePicUrl()}\n"; diff --git a/examples/getStories.php b/examples/getStories.php new file mode 100644 index 00000000..13a36919 --- /dev/null +++ b/examples/getStories.php @@ -0,0 +1,8 @@ +login(); + +$stories = $instagram->getStories(); +print_r($stories); \ No newline at end of file diff --git a/src/InstagramScraper.php b/src/InstagramScraper.php index 2d1870ae..8a44235a 100644 --- a/src/InstagramScraper.php +++ b/src/InstagramScraper.php @@ -1,17 +1,16 @@ $value) { - $url .= "&$key=$value"; + $url = str_replace('{{queryId}}', urlencode($queryId), static::GRAPH_QL_QUERY_URL); + if (!empty($parameters)) { + $query_string = http_build_query($parameters); + $url .= '&' . $query_string; } return $url; } public static function getFollowUrl($accountId) { - $url = str_replace('{{accountId}}', urlencode($accountId), Endpoints::FOLLOW_URL); + $url = str_replace('{{accountId}}', urlencode($accountId), static::FOLLOW_URL); return $url; } - + public static function getFollowersJsonLink($accountId, $count, $after = '') { - $url = str_replace('{{accountId}}', urlencode($accountId), Endpoints::FOLLOWERS_URL); + $url = str_replace('{{accountId}}', urlencode($accountId), static::FOLLOWERS_URL); $url = str_replace('{{count}}', urlencode($count), $url); - - if ($after === '') { + + if ($after === '') { $url = str_replace('&after={{after}}', '', $url); + } else { + $url = str_replace('{{after}}', urlencode($after), $url); } - else { + + return $url; + } + + public static function getFollowingJsonLink($accountId, $count, $after = '') + { + $url = str_replace('{{accountId}}', urlencode($accountId), static::FOLLOWING_URL); + $url = str_replace('{{count}}', urlencode($count), $url); + + if ($after === '') { + $url = str_replace('&after={{after}}', '', $url); + } else { $url = str_replace('{{after}}', urlencode($after), $url); } - + + return $url; + } + + public static function getUserStoriesLink() + { + $url = self::getGraphQlUrl(InstagramQueryId::USER_STORIES, ['variables' => json_encode([])]); + return $url; + } + + public static function getStoriesLink($variables) + { + $url = self::getGraphQlUrl(InstagramQueryId::STORIES, ['variables' => json_encode($variables)]); return $url; } } diff --git a/src/InstagramScraper/Instagram.php b/src/InstagramScraper/Instagram.php index d1c22812..bc172455 100644 --- a/src/InstagramScraper/Instagram.php +++ b/src/InstagramScraper/Instagram.php @@ -7,9 +7,12 @@ use InstagramScraper\Exception\InstagramNotFoundException; use InstagramScraper\Model\Account; use InstagramScraper\Model\Comment; +use InstagramScraper\Model\Like; use InstagramScraper\Model\Location; use InstagramScraper\Model\Media; +use InstagramScraper\Model\Story; use InstagramScraper\Model\Tag; +use InstagramScraper\Model\UserStories; use phpFastCache\CacheManager; use Unirest\Request; @@ -18,11 +21,20 @@ class Instagram const HTTP_NOT_FOUND = 404; const HTTP_OK = 200; const MAX_COMMENTS_PER_REQUEST = 300; + const MAX_LIKES_PER_REQUEST = 300; + const PAGING_TIME_LIMIT_SEC = 1800; // 30 mins time limit on operations that require multiple requests + const PAGING_DELAY_MINIMUM_MICROSEC = 1000000; // 1 sec min delay to simulate browser + const PAGING_DELAY_MAXIMUM_MICROSEC = 3000000; // 3 sec max delay to simulate browser private static $instanceCache; private $sessionUsername; private $sessionPassword; private $userSession; + private $userAgent = null; + + public $pagingTimeLimitSec = self::PAGING_TIME_LIMIT_SEC; + public $pagingDelayMinimumMicrosec = self::PAGING_DELAY_MINIMUM_MICROSEC; + public $pagingDelayMaximumMicrosec = self::PAGING_DELAY_MAXIMUM_MICROSEC; /** * @param string $username @@ -39,10 +51,11 @@ public static function withCredentials($username, $password, $sessionFolder = nu if (is_string($sessionFolder)) { CacheManager::setDefaultConfig([ 'path' => $sessionFolder, + 'ignoreSymfonyNotice' => true, ]); - self::$instanceCache = CacheManager::getInstance('files'); + static::$instanceCache = CacheManager::getInstance('files'); } else { - self::$instanceCache = $sessionFolder; + static::$instanceCache = $sessionFolder; } $instance = new self(); $instance->sessionUsername = $username; @@ -63,17 +76,17 @@ public static function searchTagsByTagName($tag) $response = Request::get(Endpoints::getGeneralSearchJsonLink($tag)); // use a raw constant in the code is not a good idea!! //if ($response->code === 404) { - if (self::HTTP_NOT_FOUND === $response->code) { + if (static::HTTP_NOT_FOUND === $response->code) { throw new InstagramNotFoundException('Account with given username does not exist.'); } // use a raw constant in the code is not a good idea!! //if ($response->code !== 200) { - if (self::HTTP_OK !== $response->code) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + if (static::HTTP_OK !== $response->code) { + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } - $jsonResponse = json_decode($response->raw_body, true); - if (!isset($jsonResponse['status']) || $jsonResponse['status'] != 'ok') { + $jsonResponse = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); + if (!isset($jsonResponse['status']) || $jsonResponse['status'] !== 'ok') { throw new InstagramException('Response code is not equal 200. Something went wrong. Please report issue.'); } @@ -98,13 +111,13 @@ private static function getErrorBody($rawError) return $rawError; } if (is_object($rawError)) { - $str = ""; + $str = ''; foreach ($rawError as $key => $value) { - $str .= " " . $key . " => " . $value . ";"; + $str .= ' ' . $key . ' => ' . $value . ';'; } return $str; } else { - return "Unknown body format"; + return 'Unknown body format'; } } @@ -119,15 +132,15 @@ private static function getErrorBody($rawError) public function searchAccountsByUsername($username) { $response = Request::get(Endpoints::getGeneralSearchJsonLink($username), $this->generateHeaders($this->userSession)); - if (self::HTTP_NOT_FOUND === $response->code) { + if (static::HTTP_NOT_FOUND === $response->code) { throw new InstagramNotFoundException('Account with given username does not exist.'); } - if (self::HTTP_OK !== $response->code) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + if (static::HTTP_OK !== $response->code) { + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } - $jsonResponse = json_decode($response->raw_body, true); - if (!isset($jsonResponse['status']) || $jsonResponse['status'] != 'ok') { + $jsonResponse = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); + if (!isset($jsonResponse['status']) || $jsonResponse['status'] !== 'ok') { throw new InstagramException('Response code is not equal 200. Something went wrong. Please report issue.'); } if (!isset($jsonResponse['users']) || empty($jsonResponse['users'])) { @@ -141,6 +154,35 @@ public function searchAccountsByUsername($username) return $accounts; } + /** + * @param $userAgent + * + * @return string + */ + public function setUserAgent($userAgent) + { + return $this->userAgent = $userAgent; + } + + /** + * @param $userAgent + * + * @return null + */ + public function resetUserAgent($userAgent) + { + return $this->userAgent = null; + } + + /** + * + * @return string + */ + public function getUserAgent() + { + return $this->userAgent; + } + /** * @param $session * @@ -155,11 +197,17 @@ private function generateHeaders($session) $cookies .= "$key=$value; "; } $headers = [ - 'cookie' => $cookies, - 'referer' => Endpoints::BASE_URL . '/', + 'cookie' => $cookies, + 'referer' => Endpoints::BASE_URL . '/', 'x-csrftoken' => $session['csrftoken'], ]; } + + if($this->getUserAgent()) + { + $headers['user-agent'] = $this->getUserAgent(); + } + return $headers; } @@ -173,37 +221,38 @@ private function generateHeaders($session) */ public function getMedias($username, $count = 20, $maxId = '') { - $index = 0; - $medias = []; - $isMoreAvailable = true; - while ($index < $count && $isMoreAvailable) { - $response = Request::get(Endpoints::getAccountMediasJsonLink($username, $maxId), $this->generateHeaders($this->userSession)); - if (self::HTTP_OK !== $response->code) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); - } - - $arr = json_decode($response->raw_body, true); - if (!is_array($arr)) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); - } - // fix - count takes longer/has more overhead - if (empty($arr['items']) || !isset($arr['items'])) { - return []; - } - foreach ($arr['items'] as $mediaArray) { - if ($index === $count) { - return $medias; - } - $medias[] = Media::create($mediaArray); - $index++; - } - if (empty($arr['items']) || !isset($arr['items'])) { - return $medias; - } - $maxId = $arr['items'][count($arr['items']) - 1]['id']; - $isMoreAvailable = $arr['more_available']; - } - return $medias; + $account = $this->getAccount($username); + $index = 0; + $medias = []; + $isMoreAvailable = true; + while ($index < $count && $isMoreAvailable) { + $response = Request::get(Endpoints::getAccountMediasJsonLink($account->getId(), $maxId), $this->generateHeaders($this->userSession)); + if (static::HTTP_OK !== $response->code) { + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + } + $arr = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); + if (!is_array($arr)) { + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + } + $nodes = $arr['data']['user']['edge_owner_to_timeline_media']['edges']; + // fix - count takes longer/has more overhead + if (!isset($nodes) || empty($nodes)) { + return []; + } + foreach ($nodes as $mediaArray) { + if ($index === $count) { + return $medias; + } + $medias[] = Media::create($mediaArray['node']); + $index++; + } + if (empty($nodes) || !isset($nodes)) { + return $medias; + } + $maxId = $arr['data']['user']['edge_owner_to_timeline_media']['page_info']['end_cursor']; + $isMoreAvailable = $arr['data']['user']['edge_owner_to_timeline_media']['page_info']['has_next_page']; + } + return $medias; } /** @@ -232,15 +281,15 @@ public function getMediaByUrl($mediaUrl) $response = Request::get(rtrim($mediaUrl, '/') . '/?__a=1', $this->generateHeaders($this->userSession)); // use a raw constant in the code is not a good idea!! //if ($response->code === 404) { - if (self::HTTP_NOT_FOUND === $response->code) { + if (static::HTTP_NOT_FOUND === $response->code) { throw new InstagramNotFoundException('Media with given code does not exist or account is private.'); } // use a raw constant in the code is not a good idea!! //if ($response->code !== 200) { - if (self::HTTP_OK !== $response->code) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + if (static::HTTP_OK !== $response->code) { + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } - $mediaArray = json_decode($response->raw_body, true); + $mediaArray = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); if (!isset($mediaArray['graphql']['shortcode_media'])) { throw new InstagramException('Media with this code does not exist'); } @@ -271,51 +320,53 @@ public function getMediaByCode($mediaCode) */ public function getPaginateMedias($username, $maxId = '') { - $hasNextPage = true; - $medias = []; - - $toReturn = [ - 'medias' => $medias, - 'maxId' => $maxId, - 'hasNextPage' => $hasNextPage, - ]; - - $response = Request::get(Endpoints::getAccountMediasJsonLink($username, $maxId), - $this->generateHeaders($this->userSession)); - - // use a raw constant in the code is not a good idea!! - //if ($response->code !== 200) { - if (self::HTTP_OK !== $response->code) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); - } - - $arr = json_decode($response->raw_body, true); - - if (!is_array($arr)) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); - } - - //if (count($arr['items']) === 0) { - // I generally use empty. Im not sure why people would use count really - If the array is large then count takes longer/has more overhead. - // If you simply need to know whether or not the array is empty then use empty. - if (empty($arr['items'])) { - return $toReturn; - } - - foreach ($arr['items'] as $mediaArray) { - $medias[] = Media::create($mediaArray); - } - - $maxId = $arr['items'][count($arr['items']) - 1]['id']; - $hasNextPage = $arr['more_available']; - - $toReturn = [ - 'medias' => $medias, - 'maxId' => $maxId, - 'hasNextPage' => $hasNextPage, - ]; - - return $toReturn; + $account = $this->getAccount($username); + $hasNextPage = true; + $medias = []; + + $toReturn = [ + 'medias' => $medias, + 'maxId' => $maxId, + 'hasNextPage' => $hasNextPage, + ]; + + $response = Request::get(Endpoints::getAccountMediasJsonLink($account->getId(), $maxId), + $this->generateHeaders($this->userSession)); + + // use a raw constant in the code is not a good idea!! + //if ($response->code !== 200) { + if (static::HTTP_OK !== $response->code) { + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + } + + $arr = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); + + if (!is_array($arr)) { + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + } + $nodes = $arr['data']['user']['edge_owner_to_timeline_media']['edges']; + + //if (count($arr['items']) === 0) { + // I generally use empty. Im not sure why people would use count really - If the array is large then count takes longer/has more overhead. + // If you simply need to know whether or not the array is empty then use empty. + if (empty($nodes)) { + return $toReturn; + } + + foreach ($nodes as $mediaArray) { + $medias[] = Media::create($mediaArray['node']); + } + + $maxId = $arr['data']['user']['edge_owner_to_timeline_media']['page_info']['end_cursor']; + $isMoreAvailable = $arr['data']['user']['edge_owner_to_timeline_media']['page_info']['has_next_page']; + + $toReturn = [ + 'medias' => $medias, + 'maxId' => $maxId, + 'hasNextPage' => $hasNextPage, + ]; + + return $toReturn; } /** @@ -328,7 +379,7 @@ public function getPaginateMedias($username, $maxId = '') public function getMediaCommentsById($mediaId, $count = 10, $maxId = null) { $code = Media::getCodeFromId($mediaId); - return self::getMediaCommentsByCode($code, $count, $maxId); + return static::getMediaCommentsByCode($code, $count, $maxId); } /** @@ -346,10 +397,10 @@ public function getMediaCommentsByCode($code, $count = 10, $maxId = null) $index = 0; $hasPrevious = true; while ($hasPrevious && $index < $count) { - if ($remain > self::MAX_COMMENTS_PER_REQUEST) { - $numberOfCommentsToRetreive = self::MAX_COMMENTS_PER_REQUEST; - $remain -= self::MAX_COMMENTS_PER_REQUEST; - $index += self::MAX_COMMENTS_PER_REQUEST; + if ($remain > static::MAX_COMMENTS_PER_REQUEST) { + $numberOfCommentsToRetreive = static::MAX_COMMENTS_PER_REQUEST; + $remain -= static::MAX_COMMENTS_PER_REQUEST; + $index += static::MAX_COMMENTS_PER_REQUEST; } else { $numberOfCommentsToRetreive = $remain; $index += $remain; @@ -363,12 +414,12 @@ public function getMediaCommentsByCode($code, $count = 10, $maxId = null) $response = Request::get($commentsUrl, $this->generateHeaders($this->userSession)); // use a raw constant in the code is not a good idea!! //if ($response->code !== 200) { - if (self::HTTP_OK !== $response->code) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + if (static::HTTP_OK !== $response->code) { + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } - $cookies = self::parseCookies($response->headers['Set-Cookie']); + $cookies = static::parseCookies($response->headers['Set-Cookie']); $this->userSession['csrftoken'] = $cookies['csrftoken']; - $jsonResponse = json_decode($response->raw_body, true); + $jsonResponse = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); $nodes = $jsonResponse['data']['shortcode_media']['edge_media_to_comment']['edges']; foreach ($nodes as $commentArray) { $comments[] = Comment::create($commentArray['node']); @@ -387,6 +438,8 @@ public function getMediaCommentsByCode($code, $count = 10, $maxId = null) } /** + * We work only on https in this case if we have same cookies on Secure and not - we will choice Secure cookie + * * @param string $rawCookies * * @return array @@ -397,22 +450,92 @@ private static function parseCookies($rawCookies) $rawCookies = [$rawCookies]; } - $cookies = []; - foreach ($rawCookies as $c) { - $c = explode(';', $c)[0]; - $parts = explode('=', $c); + $not_secure_cookies = []; + $secure_cookies = []; + + foreach ($rawCookies as $cookie) { + $cookie_array = 'not_secure_cookies'; + $cookie_parts = explode(';', $cookie); + foreach ($cookie_parts as $cookie_part) { + if (trim($cookie_part) == 'Secure') { + $cookie_array = 'secure_cookies'; + break; + } + } + $value = array_shift($cookie_parts); + $parts = explode('=', $value); if (sizeof($parts) >= 2 && !is_null($parts[1])) { - $cookies[$parts[0]] = $parts[1]; + ${$cookie_array}[$parts[0]] = $parts[1]; } } + + $cookies = $secure_cookies + $not_secure_cookies; return $cookies; } + /** + * @param $code + * @param int $count + * @param null $maxId + * + * @return array + * @throws InstagramException + */ + public function getMediaLikesByCode($code, $count = 10, $maxId = null) + { + $remain = $count; + $likes = []; + $index = 0; + $hasPrevious = true; + while ($hasPrevious && $index < $count) { + if ($remain > self::MAX_LIKES_PER_REQUEST) { + $numberOfLikesToRetreive = self::MAX_LIKES_PER_REQUEST; + $remain -= self::MAX_LIKES_PER_REQUEST; + $index += self::MAX_LIKES_PER_REQUEST; + } else { + $numberOfLikesToRetreive = $remain; + $index += $remain; + $remain = 0; + } + if (!isset($maxId)) { + $maxId = ''; + + } + $commentsUrl = Endpoints::getLastLikesByCode($code, $numberOfLikesToRetreive, $maxId); + $response = Request::get($commentsUrl, $this->generateHeaders($this->userSession)); + if ($response->code !== 200) { + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . $response->body . ' Something went wrong. Please report issue.'); + } + $cookies = self::parseCookies($response->headers['Set-Cookie']); + $this->userSession['csrftoken'] = $cookies['csrftoken']; + $jsonResponse = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); + + $nodes = $jsonResponse['data']['shortcode_media']['edge_liked_by']['edges']; + + foreach ($nodes as $likesArray) { + $likes[] = Like::create($likesArray['node']); + } + + $hasPrevious = $jsonResponse['data']['shortcode_media']['edge_liked_by']['page_info']['has_next_page']; + $numberOfLikes = $jsonResponse['data']['shortcode_media']['edge_liked_by']['count']; + if ($count > $numberOfLikes) { + $count = $numberOfLikes; + } + if (sizeof($nodes) == 0) { + return $likes; + } + $maxId = $jsonResponse['data']['shortcode_media']['edge_liked_by']['page_info']['end_cursor']; + } + + return $likes; + } + /** * @param string $id * * @return Account * @throws InstagramException + * @throws \InvalidArgumentException */ public function getAccountById($id) { @@ -435,10 +558,10 @@ public function getAccountById($id) } if ($response->code !== 302) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->raw_body) . ' Something went wrong. Please report issue.'); + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->raw_body) . ' Something went wrong. Please report issue.'); } - $cookies = self::parseCookies($response->headers['Set-Cookie']); + $cookies = static::parseCookies($response->headers['Set-Cookie']); $this->userSession['csrftoken'] = $cookies['csrftoken']; // Get the username from the response url. @@ -459,18 +582,18 @@ public function getAccountById($id) public function getAccount($username) { $response = Request::get(Endpoints::getAccountJsonLink($username), $this->generateHeaders($this->userSession)); - if (self::HTTP_NOT_FOUND === $response->code) { + if (static::HTTP_NOT_FOUND === $response->code) { throw new InstagramNotFoundException('Account with given username does not exist.'); } - if (self::HTTP_OK !== $response->code) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + if (static::HTTP_OK !== $response->code) { + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } - $userArray = json_decode($response->raw_body, true); - if (!isset($userArray['user'])) { + $userArray = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); + if (!isset($userArray['graphql']['user'])) { throw new InstagramException('Account with this username does not exist'); } - return Account::create($userArray['user']); + return Account::create($userArray['graphql']['user']); } /** @@ -492,23 +615,24 @@ public function getMediasByTag($tag, $count = 12, $maxId = '', $minTimestamp = n $response = Request::get(Endpoints::getMediasJsonByTagLink($tag, $maxId), $this->generateHeaders($this->userSession)); if ($response->code !== 200) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } - $cookies = self::parseCookies($response->headers['Set-Cookie']); + + $cookies = static::parseCookies($response->headers['Set-Cookie']); $this->userSession['csrftoken'] = $cookies['csrftoken']; - $arr = json_decode($response->raw_body, true); + $arr = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); if (!is_array($arr)) { throw new InstagramException('Response decoding failed. Returned data corrupted or this library outdated. Please report issue'); } - if (empty($arr['tag']['media']['count'])) { + if (empty($arr['graphql']['hashtag']['edge_hashtag_to_media']['count'])) { return []; } - $nodes = $arr['tag']['media']['nodes']; + $nodes = $arr['graphql']['hashtag']['edge_hashtag_to_media']['edges']; foreach ($nodes as $mediaArray) { if ($index === $count) { return $medias; } - $media = Media::create($mediaArray); + $media = Media::create($mediaArray['node']); if (in_array($media->getId(), $mediaIds)) { return $medias; } @@ -522,8 +646,8 @@ public function getMediasByTag($tag, $count = 12, $maxId = '', $minTimestamp = n if (empty($nodes)) { return $medias; } - $maxId = $arr['tag']['media']['page_info']['end_cursor']; - $hasNextPage = $arr['tag']['media']['page_info']['has_next_page']; + $maxId = $arr['graphql']['hashtag']['edge_hashtag_to_media']['page_info']['end_cursor']; + $hasNextPage = $arr['graphql']['hashtag']['edge_hashtag_to_media']['page_info']['has_next_page']; } return $medias; } @@ -550,35 +674,35 @@ public function getPaginateMediasByTag($tag, $maxId = '') $this->generateHeaders($this->userSession)); if ($response->code !== 200) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } - $cookies = self::parseCookies($response->headers['Set-Cookie']); + $cookies = static::parseCookies($response->headers['Set-Cookie']); $this->userSession['csrftoken'] = $cookies['csrftoken']; - $arr = json_decode($response->raw_body, true); + $arr = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); if (!is_array($arr)) { throw new InstagramException('Response decoding failed. Returned data corrupted or this library outdated. Please report issue'); } - if (empty($arr['tag']['media']['count'])) { + if (empty($arr['graphql']['hashtag']['edge_hashtag_to_media']['count'])) { return $toReturn; } - $nodes = $arr['tag']['media']['nodes']; + $nodes = $arr['graphql']['hashtag']['edge_hashtag_to_media']['edges']; if (empty($nodes)) { return $toReturn; } foreach ($nodes as $mediaArray) { - $medias[] = Media::create($mediaArray); + $medias[] = Media::create($mediaArray['node']); } - $maxId = $arr['tag']['media']['page_info']['end_cursor']; - $hasNextPage = $arr['tag']['media']['page_info']['has_next_page']; - $count = $arr['tag']['media']['count']; + $maxId = $arr['graphql']['hashtag']['edge_hashtag_to_media']['page_info']['end_cursor']; + $hasNextPage = $arr['graphql']['hashtag']['edge_hashtag_to_media']['page_info']['has_next_page']; + $count = $arr['graphql']['hashtag']['edge_hashtag_to_media']['count']; $toReturn = [ 'medias' => $medias, @@ -605,14 +729,15 @@ public function getCurrentTopMediasByTagName($tagName) throw new InstagramNotFoundException('Account with given username does not exist.'); } if ($response->code !== 200) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } - $cookies = self::parseCookies($response->headers['Set-Cookie']); + $cookies = static::parseCookies($response->headers['Set-Cookie']); $this->userSession['csrftoken'] = $cookies['csrftoken']; - $jsonResponse = json_decode($response->raw_body, true); + $jsonResponse = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); $medias = []; - foreach ($jsonResponse['tag']['top_posts']['nodes'] as $mediaArray) { - $medias[] = Media::create($mediaArray); + $nodes = (array) @$jsonResponse['graphql']['hashtag']['edge_hashtag_to_media']['edges']; + foreach ($nodes as $mediaArray) { + $medias[] = Media::create($mediaArray['node']); } return $medias; } @@ -632,11 +757,11 @@ public function getCurrentTopMediasByLocationId($facebookLocationId) throw new InstagramNotFoundException('Location with this id doesn\'t exist'); } if ($response->code !== 200) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } - $cookies = self::parseCookies($response->headers['Set-Cookie']); + $cookies = static::parseCookies($response->headers['Set-Cookie']); $this->userSession['csrftoken'] = $cookies['csrftoken']; - $jsonResponse = json_decode($response->raw_body, true); + $jsonResponse = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); $nodes = $jsonResponse['location']['top_posts']['nodes']; $medias = []; foreach ($nodes as $mediaArray) { @@ -662,11 +787,11 @@ public function getMediasByLocationId($facebookLocationId, $quantity = 12, $offs $response = Request::get(Endpoints::getMediasJsonByLocationIdLink($facebookLocationId, $offset), $this->generateHeaders($this->userSession)); if ($response->code !== 200) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } - $cookies = self::parseCookies($response->headers['Set-Cookie']); + $cookies = static::parseCookies($response->headers['Set-Cookie']); $this->userSession['csrftoken'] = $cookies['csrftoken']; - $arr = json_decode($response->raw_body, true); + $arr = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); $nodes = $arr['location']['media']['nodes']; foreach ($nodes as $mediaArray) { if ($index === $quantity) { @@ -699,11 +824,11 @@ public function getLocationById($facebookLocationId) throw new InstagramNotFoundException('Location with this id doesn\'t exist'); } if ($response->code !== 200) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } - $cookies = self::parseCookies($response->headers['Set-Cookie']); + $cookies = static::parseCookies($response->headers['Set-Cookie']); $this->userSession['csrftoken'] = $cookies['csrftoken']; - $jsonResponse = json_decode($response->raw_body, true); + $jsonResponse = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); return Location::create($jsonResponse['location']); } @@ -719,7 +844,7 @@ public function getLocationById($facebookLocationId) public function getFollowers($accountId, $count = 20, $pageSize = 20, $delayed = true) { if ($delayed) { - set_time_limit(1800); // 30 mins + set_time_limit($this->pagingTimeLimitSec); } $index = 0; @@ -734,10 +859,10 @@ public function getFollowers($accountId, $count = 20, $pageSize = 20, $delayed = $response = Request::get(Endpoints::getFollowersJsonLink($accountId, $pageSize, $endCursor), $this->generateHeaders($this->userSession)); if ($response->code !== 200) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } - $jsonResponse = json_decode($response->raw_body, true); + $jsonResponse = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); if ($jsonResponse['data']['user']['edge_followed_by']['count'] === 0) { return $accounts; @@ -765,35 +890,156 @@ public function getFollowers($accountId, $count = 20, $pageSize = 20, $delayed = if ($delayed) { // Random wait between 1 and 3 sec to mimic browser - $microsec = rand(1000000, 3000000); + $microsec = rand($this->pagingDelayMinimumMicrosec, $this->pagingDelayMaximumMicrosec); usleep($microsec); } } return $accounts; } + /** + * @param string $accountId Account id of the profile to query + * @param int $count Total followed accounts to retrieve + * @param int $pageSize Internal page size for pagination + * @param bool $delayed Use random delay between requests to mimic browser behaviour + * + * @return array + * @throws InstagramException + */ + public function getFollowing($accountId, $count = 20, $pageSize = 20, $delayed = true) + { + if ($delayed) { + set_time_limit($this->pagingTimeLimitSec); + } + + $index = 0; + $accounts = []; + $endCursor = ''; + + if ($count < $pageSize) { + throw new InstagramException('Count must be greater than or equal to page size.'); + } + + while (true) { + $response = Request::get(Endpoints::getFollowingJsonLink($accountId, $pageSize, $endCursor), + $this->generateHeaders($this->userSession)); + if ($response->code !== 200) { + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + } + + $jsonResponse = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); + + if ($jsonResponse['data']['user']['edge_follow']['count'] === 0) { + return $accounts; + } + + $edgesArray = $jsonResponse['data']['user']['edge_follow']['edges']; + if (count($edgesArray) === 0) { + throw new InstagramException('Failed to get followers of account id ' . $accountId . '. The account is private.'); + } + + foreach ($edgesArray as $edge) { + $accounts[] = $edge['node']; + $index++; + if ($index >= $count) { + break 2; + } + } + + $pageInfo = $jsonResponse['data']['user']['edge_follow']['page_info']; + if ($pageInfo['has_next_page']) { + $endCursor = $pageInfo['end_cursor']; + } else { + break; + } + + if ($delayed) { + // Random wait between 1 and 3 sec to mimic browser + $microsec = rand($this->pagingDelayMinimumMicrosec, $this->pagingDelayMaximumMicrosec); + usleep($microsec); + } + } + return $accounts; + } + + /** + * @param array $reel_ids - array of instagram user ids + * @return array + * @throws InstagramException + */ + public function getStories($reel_ids = null) + { + $variables = ['precomposed_overlay' => false, 'reel_ids' => []]; + if (empty($reel_ids)) { + $response = Request::get(Endpoints::getUserStoriesLink(), + $this->generateHeaders($this->userSession)); + + if ($response->code !== 200) { + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + } + + $jsonResponse = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); + if (empty($jsonResponse['data']['user']['feed_reels_tray']['edge_reels_tray_to_reel']['edges'])) { + return []; + } + + foreach ($jsonResponse['data']['user']['feed_reels_tray']['edge_reels_tray_to_reel']['edges'] as $edge) { + $variables['reel_ids'][] = $edge['node']['id']; + } + } else { + $variables['reel_ids'] = $reel_ids; + } + + $response = Request::get(Endpoints::getStoriesLink($variables), + $this->generateHeaders($this->userSession)); + + if ($response->code !== 200) { + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + } + + $jsonResponse = json_decode($response->raw_body, true, 512, JSON_BIGINT_AS_STRING); + + if (empty($jsonResponse['data']['reels_media'])) { + return []; + } + + $stories = []; + foreach ($jsonResponse['data']['reels_media'] as $user) { + $UserStories = UserStories::create(); + $UserStories->setOwner(Account::create($user['user'])); + foreach ($user['items'] as $item) { + $UserStories->addStory(Story::create($item)); + } + $stories[] = $UserStories; + } + return $stories; + } + /** * @param bool $force + * @param bool $support_two_step_verification + * + * $support_two_step_verification true works only in cli mode - just run login in cli mode - save cookie to file and use in any mode * * @throws InstagramAuthException * @throws InstagramException * * @return array */ - public function login($force = false) + public function login($force = false, $support_two_step_verification = false) { if ($this->sessionUsername == null || $this->sessionPassword == null) { throw new InstagramAuthException("User credentials not provided"); } - $cachedString = self::$instanceCache->getItem($this->sessionUsername); + $cachedString = static::$instanceCache->getItem($this->sessionUsername); $session = $cachedString->get(); if ($force || !$this->isLoggedIn($session)) { $response = Request::get(Endpoints::BASE_URL); if ($response->code !== 200) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . static::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } - $cookies = self::parseCookies($response->headers['Set-Cookie']); + $cookies = static::parseCookies($response->headers['Set-Cookie']); $mid = $cookies['mid']; $csrfToken = $cookies['csrftoken']; $headers = ['cookie' => "csrftoken=$csrfToken; mid=$mid;", @@ -804,7 +1050,9 @@ public function login($force = false) ['username' => $this->sessionUsername, 'password' => $this->sessionPassword]); if ($response->code !== 200) { - if ((is_string($response->code) || is_numeric($response->code)) && is_string($response->body)) { + if ($response->code === 400 && isset($response->body->message) && $response->body->message == 'checkpoint_required' && $support_two_step_verification) { + $response = $this->verifyTwoStep($response, $cookies); + } elseif ((is_string($response->code) || is_numeric($response->code)) && is_string($response->body)) { throw new InstagramAuthException('Response code is ' . $response->code . '. Body: ' . $response->body . ' Something went wrong. Please report issue.'); } else { throw new InstagramAuthException('Something went wrong. Please report issue.'); @@ -817,10 +1065,10 @@ public function login($force = false) } } - $cookies = self::parseCookies($response->headers['Set-Cookie']); + $cookies = static::parseCookies($response->headers['Set-Cookie']); $cookies['mid'] = $mid; $cachedString->set($cookies); - self::$instanceCache->save($cachedString); + static::$instanceCache->save($cachedString); $this->userSession = $cookies; } else { $this->userSession = $session; @@ -829,6 +1077,87 @@ public function login($force = false) return $this->generateHeaders($this->userSession); } + private function verifyTwoStep($response, $cookies) + { + $new_cookies = static::parseCookies($response->headers['Set-Cookie']); + $cookies = array_merge($cookies, $new_cookies); + $cookie_string = ''; + foreach ($cookies as $name => $value) { + $cookie_string .= $name . "=" . $value . "; "; + } + $headers = [ + 'cookie' => $cookie_string, + 'referer' => Endpoints::LOGIN_URL, + 'x-csrftoken' => $cookies['csrftoken'] + ]; + + $url = Endpoints::BASE_URL . $response->body->checkpoint_url; + $response = Request::get($url, $headers); + if (preg_match('/window._sharedData\s\=\s(.*?)\;<\/script>/', $response->raw_body, $matches)) { + $data = json_decode($matches[1], true, 512, JSON_BIGINT_AS_STRING); + if (!empty($data['entry_data']['Challenge'][0]['extraData']['content'][3]['fields'][0]['values'])) { + $choices = $data['entry_data']['Challenge'][0]['extraData']['content'][3]['fields'][0]['values']; + } elseif (!empty($data['entry_data']['Challenge'][0]['fields'])) { + $fields = $data['entry_data']['Challenge'][0]['fields']; + if (!empty($fields['email'])) { + $choices[] = ['label' => 'Email: ' . $fields['email'], 'value' => 1]; + } + if (!empty($fields['phone_number'])) { + $choices[] = ['label' => 'Phone: ' . $fields['phone_number'], 'value' => 0]; + } + } + + if (!empty($choices)) { + if (count($choices) > 1) { + $possible_values = []; + print "Select where to send security code\n"; + foreach ($choices as $choice) { + print $choice['label'] . " - " . $choice['value'] . "\n"; + $possible_values[$choice['value']] = true; + } + + $selected_choice = null; + while (empty($possible_values[$selected_choice])) { + if ($selected_choice) { + print "Wrong choice. Try again\n"; + } + print "Your choice: "; + $selected_choice = trim(fgets(STDIN)); + } + } else { + print "Message with security code sent to: " . $choices[0]['label'] . "\n"; + $selected_choice = $choices[0]['value']; + } + $response = Request::post($url, $headers, ['choice' => $selected_choice]); + } + } + + if (!preg_match('/name="security_code"/', $response->raw_body, $matches)) { + throw new InstagramAuthException('Something went wrong when try two step verification. Please report issue.'); + } + + $security_code = null; + while (strlen($security_code) != 6 && !is_int($security_code)) { + if ($security_code) { + print "Wrong security code\n"; + } + print "Enter security code: "; + $security_code = trim(fgets(STDIN)); + } + $post_data = [ + 'csrfmiddlewaretoken' => $cookies['csrftoken'], + 'verify' => 'Verify Account', + 'security_code' => $security_code, + ]; + + $response = Request::post($url, $headers, $post_data); + if ($response->code !== 200) { + throw new InstagramAuthException('Something went wrong when try two step verification and enter security code. Please report issue.'); + } + + return $response; + } + /** * @param $session * @@ -849,7 +1178,7 @@ public function isLoggedIn($session) if ($response->code !== 200) { return false; } - $cookies = self::parseCookies($response->headers['Set-Cookie']); + $cookies = static::parseCookies($response->headers['Set-Cookie']); if (!isset($cookies['ds_user_id'])) { return false; } @@ -861,8 +1190,46 @@ public function isLoggedIn($session) */ public function saveSession() { - $cachedString = self::$instanceCache->getItem($this->sessionUsername); + $cachedString = static::$instanceCache->getItem($this->sessionUsername); $cachedString->set($this->userSession); } + /** + * @param array $config + */ + public static function setProxy(array $config) + { + $defaultConfig = [ + 'port' => false, + 'tunnel' => false, + 'address' => false, + 'type' => CURLPROXY_HTTP, + 'timeout' => false, + 'auth' => [ + 'user' => '', + 'pass' => '', + 'method' => CURLAUTH_BASIC + ], + ]; + + $config = array_replace($defaultConfig, $config); + + Request::proxy($config['address'], $config['port'], $config['type'], $config['tunnel']); + + if (isset($config['auth'])) { + Request::proxyAuth($config['auth']['user'], $config['auth']['pass'], $config['auth']['method']); + } + + if (isset($config['timeout'])) { + Request::timeout((int)$config['timeout']); + } + } + + /** + * Disable proxy for all requests + */ + public static function disableProxy() + { + Request::proxy(''); + } } diff --git a/src/InstagramScraper/InstagramQueryId.php b/src/InstagramScraper/InstagramQueryId.php index 4ee5d2b0..8a45ce06 100644 --- a/src/InstagramScraper/InstagramQueryId.php +++ b/src/InstagramScraper/InstagramQueryId.php @@ -9,5 +9,7 @@ class InstagramQueryId * id = {{accoundId}}, first = {{count}}, after = {{mediaId}} */ const USER_MEDIAS = '17880160963012870'; + const USER_STORIES = '17890626976041463'; + const STORIES = '17873473675158481'; } \ No newline at end of file diff --git a/src/InstagramScraper/Model/Account.php b/src/InstagramScraper/Model/Account.php index 439a9b84..01f76e00 100644 --- a/src/InstagramScraper/Model/Account.php +++ b/src/InstagramScraper/Model/Account.php @@ -32,6 +32,12 @@ class Account extends AbstractModel */ protected $profilePicUrl = ''; + /** + * Profile picture url HD + * @var string + */ + protected $profilePicUrlHd = ''; + /** * Information filled by user * @var string @@ -100,7 +106,11 @@ public function getUsername() */ public function getId() { - return (int)$this->id; + if (PHP_INT_SIZE > 4) { + $this->id = (int)$this->id; + } + + return $this->id; } /** @@ -119,6 +129,20 @@ public function getProfilePicUrl() return $this->profilePicUrl; } + /** + * @return string + */ + public function getProfilePicUrlHd() + { + $toReturn = $this->profilePicUrl; + + if ($this->profilePicUrlHd !== '') { + $toReturn = $this->profilePicUrlHd; + } + + return $toReturn; + } + /** * @return string */ @@ -184,7 +208,7 @@ protected function initPropertiesCustom($value, $prop, $array) { switch ($prop) { case 'id': - $this->id = (int)$value; + $this->id = $value; break; case 'username': $this->username = $value; @@ -195,19 +219,22 @@ protected function initPropertiesCustom($value, $prop, $array) case 'profile_pic_url': $this->profilePicUrl = $value; break; + case 'profile_pic_url_hd': + $this->profilePicUrlHd = $value; + break; case 'biography': $this->biography = $value; break; case 'external_url': $this->externalUrl = $value; break; - case 'follows': + case 'edge_follow': $this->followsCount = !empty($array[$prop]['count']) ? (int)$array[$prop]['count'] : 0; break; - case 'followed_by': + case 'edge_followed_by': $this->followedByCount = !empty($array[$prop]['count']) ? (int)$array[$prop]['count'] : 0; break; - case 'media': + case 'edge_owner_to_timeline_media': $this->mediaCount = !empty($array[$prop]['count']) ? $array[$prop]['count'] : 0; break; case 'is_private': @@ -218,4 +245,4 @@ protected function initPropertiesCustom($value, $prop, $array) break; } } -} \ No newline at end of file +} diff --git a/src/InstagramScraper/Model/Like.php b/src/InstagramScraper/Model/Like.php new file mode 100644 index 00000000..9d46e2b8 --- /dev/null +++ b/src/InstagramScraper/Model/Like.php @@ -0,0 +1,50 @@ +id; + } + + /** + * @return mixed + */ + public function getUserName() + { + return $this->username; + } + + /** + * @param $value + * @param $prop + */ + protected function initPropertiesCustom($value, $prop) + { + switch ($prop) { + case 'id': + $this->id = $value; + break; + case 'username': + $this->username = $value; + break; + } + } + +} \ No newline at end of file diff --git a/src/InstagramScraper/Model/Location.php b/src/InstagramScraper/Model/Location.php index 1178c57a..c5b2b243 100644 --- a/src/InstagramScraper/Model/Location.php +++ b/src/InstagramScraper/Model/Location.php @@ -15,6 +15,7 @@ class Location extends AbstractModel 'slug' => 'slug', 'lat' => 'lat', 'lng' => 'lng', + 'modified' => 'modified' ]; /** * @var @@ -45,6 +46,11 @@ class Location extends AbstractModel */ protected $isLoaded = false; + /** + * @var + */ + protected $modified; + /** * @return mixed */ @@ -92,4 +98,12 @@ public function getLat() { return $this->lat; } -} \ No newline at end of file + + /** + * @return mixed + */ + public function getModified() + { + return $this->modified; + } +} diff --git a/src/InstagramScraper/Model/Media.php b/src/InstagramScraper/Model/Media.php index 4da84499..79506d4a 100644 --- a/src/InstagramScraper/Model/Media.php +++ b/src/InstagramScraper/Model/Media.php @@ -60,6 +60,11 @@ class Media extends AbstractModel */ protected $imageHighResolutionUrl = ''; + /** + * @var array + */ + protected $squareThumbnailsUrl = []; + /** * @var array */ @@ -130,6 +135,11 @@ class Media extends AbstractModel */ protected $commentsCount = 0; + /** + * @var Media[]|array + */ + protected $sidecarMedias = []; + /** * @param string $code * @@ -248,6 +258,15 @@ public function getImageHighResolutionUrl() return $this->imageHighResolutionUrl; } + + /** + * @return array + */ + public function getSquareThumbnailsUrl() { + return $this->squareThumbnailsUrl; + } + + /** * @return array */ @@ -352,6 +371,14 @@ public function getCommentsCount() return $this->commentsCount; } + /** + * @return Media[]|array + */ + public function getSidecarMedias() + { + return $this->sidecarMedias; + } + /** * @param $value * @param $prop @@ -370,6 +397,7 @@ protected function initPropertiesCustom($value, $prop, $arr) break; case 'code': $this->shortCode = $value; + $this->link = Endpoints::getMediaPageLink($this->shortCode); break; case 'link': $this->link = $value; @@ -387,6 +415,13 @@ protected function initPropertiesCustom($value, $prop, $arr) $this->imageStandardResolutionUrl = $images['standard']; $this->imageHighResolutionUrl = $images['high']; break; + case 'thumbnail_resources': + $thumbnailsUrl = []; + foreach( $value as $thumbnail ) { + $thumbnailsUrl[] = $thumbnail['src']; + } + $this->squareThumbnailsUrl = $thumbnailsUrl; + break; case 'carousel_media': $this->type = self::TYPE_CAROUSEL; $this->carouselMedia = []; @@ -399,28 +434,34 @@ protected function initPropertiesCustom($value, $prop, $arr) break; case 'video_views': $this->videoViews = $value; + $this->type = static::TYPE_VIDEO; break; case 'videos': $this->videoLowResolutionUrl = $arr[$prop]['low_resolution']['url']; $this->videoStandardResolutionUrl = $arr[$prop]['standard_resolution']['url']; $this->videoLowBandwidthUrl = $arr[$prop]['low_bandwidth']['url']; break; - case 'location': - switch ($prop) { - case 'id': - $this->locationId = $value[$prop]; - break; - case 'name': - $this->locationId = $value[$prop]; - break; + case 'video_resources': + foreach ($value as $video) { + if ($video['profile'] == 'MAIN') { + $this->videoStandardResolutionUrl = $video['src']; + } elseif ($video['profile'] == 'BASELINE') { + $this->videoLowResolutionUrl = $video['src']; + $this->videoLowBandwidthUrl = $video['src']; + } } + break; + case 'location': + $this->locationId = $arr[$prop]['id']; $this->locationName = $arr[$prop]['name']; break; case 'user': $this->owner = Account::create($arr[$prop]); break; case 'is_video': - $this->type = self::TYPE_VIDEO; + if ((bool)$value) { + $this->type = static::TYPE_VIDEO; + } break; case 'video_url': $this->videoStandardResolutionUrl = $value; @@ -443,8 +484,13 @@ protected function initPropertiesCustom($value, $prop, $arr) break; case 'edge_media_to_comment': $this->commentsCount = $arr[$prop]['count']; + break; + case 'edge_media_preview_like': $this->likesCount = $arr[$prop]['count']; break; + case 'edge_liked_by': + $this->likesCount = $arr[$prop]['count']; + break; case 'display_url': $images = self::getImageUrls($arr[$prop]); $this->imageStandardResolutionUrl = $images['standard']; @@ -453,7 +499,27 @@ protected function initPropertiesCustom($value, $prop, $arr) $this->imageThumbnailUrl = $images['thumbnail']; break; case 'edge_media_to_caption': - $this->caption = $arr[$prop]['edges'][0]['node']['text']; + if (is_array($arr[$prop]['edges']) && !empty($arr[$prop]['edges'])) { + $first_caption = $arr[$prop]['edges'][0]; + if (is_array($first_caption) && isset($first_caption['node'])) { + if (is_array($first_caption['node']) && isset($first_caption['node']['text'])) { + $this->caption = $arr[$prop]['edges'][0]['node']['text']; + } + } + } + break; + case 'edge_sidecar_to_children': + if (!is_array($arr[$prop]['edges'])) { + break; + } + + foreach ($arr[$prop]['edges'] as $edge) { + if (!isset($edge['node'])) { + continue; + } + + $this->sidecarMedias[] = static::create($edge['node']); + } break; case 'owner': $this->owner = Account::create($arr[$prop]); @@ -462,12 +528,23 @@ protected function initPropertiesCustom($value, $prop, $arr) $this->createdTime = (int)$value; break; case 'display_src': - $images = self::getImageUrls($value); + $images = static::getImageUrls($value); $this->imageStandardResolutionUrl = $images['standard']; $this->imageLowResolutionUrl = $images['low']; $this->imageHighResolutionUrl = $images['high']; $this->imageThumbnailUrl = $images['thumbnail']; - $this->type = self::TYPE_IMAGE; + if (!isset($this->type)) { + $this->type = static::TYPE_IMAGE; + } + break; + case '__typename': + if ($value == 'GraphImage') { + $this->type = static::TYPE_IMAGE; + } else if ($value == 'GraphVideo') { + $this->type = static::TYPE_VIDEO; + } else if ($value == 'GraphSidecar') { + $this->type = static::TYPE_SIDECAR; + } break; } if (!$this->ownerId && !is_null($this->owner)) { diff --git a/src/InstagramScraper/Model/Story.php b/src/InstagramScraper/Model/Story.php new file mode 100644 index 00000000..c9de1dab --- /dev/null +++ b/src/InstagramScraper/Model/Story.php @@ -0,0 +1,30 @@ + true, + ]; + + /*** + * We do not need some values - do not parse it for Story, + * for example - we do not need owner object inside story + * + * @param $value + * @param $prop + * @param $arr + */ + protected function initPropertiesCustom($value, $prop, $arr) + { + if (!empty($this->skip_prop[$prop])) { + return; + } + parent::initPropertiesCustom($value, $prop, $arr); + } +} \ No newline at end of file diff --git a/src/InstagramScraper/Model/UserStories.php b/src/InstagramScraper/Model/UserStories.php new file mode 100644 index 00000000..180d5c93 --- /dev/null +++ b/src/InstagramScraper/Model/UserStories.php @@ -0,0 +1,41 @@ +owner = $owner; + } + + public function getOwner() + { + return $this->owner; + } + + public function addStory($story) + { + $this->stories[] = $story; + } + + public function setStories($stories) + { + $this->stories = $stories; + } + + public function getStories() + { + return $this->stories; + } +} \ No newline at end of file diff --git a/src/InstagramScraper/Traits/ArrayLikeTrait.php b/src/InstagramScraper/Traits/ArrayLikeTrait.php index 78cf8c3d..6f34ae92 100644 --- a/src/InstagramScraper/Traits/ArrayLikeTrait.php +++ b/src/InstagramScraper/Traits/ArrayLikeTrait.php @@ -36,10 +36,14 @@ public function offsetGet($offset) if ($run = $this->isMethod($offset, 'get')) { return $this->run($run); } elseif (\property_exists($this, $offset)) { - return $this->{$offset}; - } else { - return null; + if (isset($this->{$offset})) { + return $this->{$offset}; + } elseif (isset($this::$offset)) { + return $this::$offset; + } } + + return null; } /** @@ -106,4 +110,4 @@ protected function run($method) return \call_user_func([$this, $method]); } -} \ No newline at end of file +} diff --git a/src/InstagramScraper/Traits/InitializerTrait.php b/src/InstagramScraper/Traits/InitializerTrait.php index 482d7c4d..09d3067d 100644 --- a/src/InstagramScraper/Traits/InitializerTrait.php +++ b/src/InstagramScraper/Traits/InitializerTrait.php @@ -50,24 +50,6 @@ trait InitializerTrait */ protected $data = []; - /** - * @param array $params - * - * @return static - */ - public static function create(array $params = null) - { - return new static($params); - } - - /** - * @return $this - */ - public static function fake() - { - return static::create()->setFake(true); - } - /** * @param array $props */ @@ -86,52 +68,38 @@ protected function __construct(array $props = null) } /** - * @return bool - */ - public function isNotEmpty() - { - return !$this->isLoadEmpty; - } - - /** - * @return bool + * @return $this */ - public function isFake() + protected function beforeInit() { - return $this->isFake; + return $this; } /** - * @return array + * @return $this */ - public function toArray() + final protected function initAuto() { - $ret = []; - $map = static::$initPropertiesMap; - foreach ($map as $key => $init) { - if (\property_exists($this, $key)) { - //if there is property then it just assign value - $ret[$key] = $this->{$key}; - } elseif (isset($this[$key])) { - //probably array access - $ret[$key] = $this[$key]; - } else { - $ret[$key] = null; + foreach ($this as $prop => $value) { + if (isset(static::$initPropertiesMap[$prop]) and $methodOrProp = static::$initPropertiesMap[$prop] and \method_exists($this, + $methodOrProp) + ) { + //if there is method then use it firstly + \call_user_func([$this, $methodOrProp], $value, $prop); } } + $this->isNew = false; + $this->isLoaded = true; + $this->isLoadEmpty = false; - return $ret; + return $this; } /** - * @param bool $value - * * @return $this */ - protected function setFake($value = true) + protected function initDefaults() { - $this->isFake = (bool) $value; - return $this; } @@ -175,45 +143,77 @@ final protected function init(array $props) /** * @return $this */ - final protected function initAuto() + protected function afterInit() { - foreach ($this as $prop => $value) { - if (isset(static::$initPropertiesMap[$prop]) and $methodOrProp = static::$initPropertiesMap[$prop] and \method_exists($this, - $methodOrProp) - ) { - //if there is method then use it firstly - \call_user_func([$this, $methodOrProp], $value, $prop); - } - } - $this->isNew = false; - $this->isLoaded = true; - $this->isLoadEmpty = false; - return $this; } /** * @return $this */ - protected function initDefaults() + public static function fake() { - return $this; + return static::create()->setFake(true); } /** + * @param bool $value + * * @return $this */ - protected function beforeInit() + protected function setFake($value = true) { + $this->isFake = (bool)$value; + return $this; } /** - * @return $this + * @param array $params + * + * @return static */ - protected function afterInit() + public static function create(array $params = null) { - return $this; + return new static($params); + } + + /** + * @return bool + */ + public function isNotEmpty() + { + return !$this->isLoadEmpty; + } + + /** + * @return bool + */ + public function isFake() + { + return $this->isFake; + } + + /** + * @return array + */ + public function toArray() + { + $ret = []; + $map = static::$initPropertiesMap; + foreach ($map as $key => $init) { + if (\property_exists($this, $key)) { + //if there is property then it just assign value + $ret[$key] = $this->{$key}; + } elseif (isset($this[$key])) { + //probably array access + $ret[$key] = $this[$key]; + } else { + $ret[$key] = null; + } + } + + return $ret; } /** @@ -240,7 +240,32 @@ protected function initDatetime($date, $key) } /** - * @param mixed $value + * @param $value + * @param $key + * + * @return $this + */ + protected function initProperty($value, $key) + { + $keys = \func_get_args(); + unset($keys[0]); //remove value + if (\count($keys) > 1) { + foreach ($keys as $key) { + if (\property_exists($this, $key)) { //first found set + $this->{$key} = $value; + + return $this; + } + } + } elseif (\property_exists($this, $key)) { + $this->{$key} = $value; + } + + return $this; + } + + /** + * @param mixed $value * @param string $key * * @return $this @@ -251,25 +276,25 @@ protected function initBool($value, $key) } /** - * @param mixed $value + * @param mixed $value * @param string $key * * @return $this */ protected function initInt($value, $key) { - return $this->initProperty((int) $value, $key); + return $this->initProperty((int)$value, $key); } /** - * @param mixed $value + * @param mixed $value * @param string $key * * @return $this */ protected function initFloat($value, $key) { - return $this->initProperty((float) $value, $key); + return $this->initProperty((float)$value, $key); } /** @@ -280,23 +305,23 @@ protected function initFloat($value, $key) */ protected function initJsonArray($rawData, $key) { - $value = \json_decode($rawData, true); + $value = \json_decode($rawData, true, 512, JSON_BIGINT_AS_STRING); if (empty($value)) { //could not resolve - if ('null' === $rawData or '' === $rawData) { $value = []; } else { - $value = (array) $rawData; + $value = (array)$rawData; } } else { - $value = (array) $value; + $value = (array)$value; } return $this->initProperty($value, $key); } /** - * @param mixed $value + * @param mixed $value * @param string $key * * @return $this @@ -306,29 +331,4 @@ protected function initExplode($value, $key) return $this->initProperty(\explode(',', $value), "is{$key}", $key); } - /** - * @param $value - * @param $key - * - * @return $this - */ - protected function initProperty($value, $key) - { - $keys = \func_get_args(); - unset($keys[0]); //remove value - if (\count($keys) > 1) { - foreach ($keys as $key) { - if (\property_exists($this, $key)) { //first found set - $this->{$key} = $value; - - return $this; - } - } - } elseif (\property_exists($this, $key)) { - $this->{$key} = $value; - } - - return $this; - } - } \ No newline at end of file diff --git a/tests/InstagramTest.php b/tests/InstagramTest.php index 1fe22ed4..b28698ac 100644 --- a/tests/InstagramTest.php +++ b/tests/InstagramTest.php @@ -19,7 +19,7 @@ public static function setUpBeforeClass() 'path' => $sessionFolder ]); $instanceCache = CacheManager::getInstance('files'); - self::$instagram = Instagram::withCredentials('raiym', 'uvebzdxgbkt2T5_K', $instanceCache); + self::$instagram = Instagram::withCredentials('raiym', 'youneverknow', $instanceCache); self::$instagram->login(); } @@ -78,13 +78,13 @@ public function testGetMediaByUrl() public function testGetLocationTopMediasById() { - $medias = self::$instagram->getLocationTopMediasById(1); + $medias = self::$instagram->getCurrentTopMediasByTagName(1); $this->assertEquals(9, count($medias)); } public function testGetLocationMediasById() { - $medias = self::$instagram->getLocationMediasById(1); + $medias = self::$instagram->getMediasByLocationId(1); $this->assertEquals(12, count($medias)); } @@ -96,7 +96,7 @@ public function testGetLocationById() public function testGetMediaByTag() { - $medias = self::$instagram->getTopMediasByTagName('hello'); + $medias = self::$instagram->getMediasByTag('hello'); echo json_encode($medias); }