diff --git a/.gitignore b/.gitignore index 339e871f..19754124 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,4 @@ composer.phar composer.lock .DS_Store phpunit.phar -index.php +tests/sessions \ No newline at end of file diff --git a/README.md b/README.md index 712aec70..35cc551c 100644 --- a/README.md +++ b/README.md @@ -1,180 +1,46 @@ -# instagram-php-scraper -# Usage +# Instagram PHP Scrapper +This library based on Instagram web version. We develop it because nowadays it is hard to get approved Instagram application. +The purpose support every feature that web desktop and mobile version support. -`composer require raiym/instagram-php-scraper` - - -```php -use InstagramScraper\Instagram; - -``` - -### Get account info -```php -$account = Instagram::getAccount('kevin'); -/* -Available properties: - $username; - $followsCount; - $followedByCount; - $profilePicUrl; - $id; - $biography; - $fullName; - $mediaCount; - $isPrivate; - $externalUrl; -*/ -echo $account->followedByCount; -``` -### Get account info by userId +## Code Example ```php -$account = Instagram::getAccountById(193886659); -echo $account->username; +$instagram = Instagram::withCredentials('username', 'password'); +$instagram->login(); +$account = $instagram->getAccountById(3); +echo $account->getUsername(); ``` - -### Search users by username +Some methods does not require auth: ```php -$users = Instagram::searchAccountsByUsername('durov'); -echo '
';
-echo json_encode($users);
-echo '

'; +$instagram = new Instagram(); +$nonPrivateAccountMedias = $instagram->getMedias('kevin'); +echo $nonPrivateAccountMedias[0]->getLink(); ``` +If you use auth it is recommended to cash user session, in this case you don't need run `$instagram->login()` method every time your program runs: -### Get account medias ```php -$medias = Instagram::getMedias('kevin', 150); - -/* -Available properties: - $id; - $createdTime; - $type; - $link; - $imageLowResolutionUrl; - $imageThumbnailUrl; - $imageStandardResolutionUrl; - $imageHighResolutionUrl; - $caption; - $captionIsEdited; - $isAd; - $videoLowResolutionUrl; - $videoStandardResolutionUrl; - $videoLowBandwidthUrl; - $videoViews; - $code; - $owner; - $ownerId; - $likesCount; - $locationId; - $locationName; - $commentsCount; - -*/ -echo $medias[0]->imageHighResolutionUrl; -echo $medias[0]->caption; - +$instagram = Instagram::withCredentials('username', 'password', '/path/to/cache/folder/'); +$instagram->login(); // will use cached session if you can force login $instagram->login(true) +$account = $instagram->getAccountById(3); +echo $account->getUsername(); ``` -### Paginate medias -```php -$result = Instagram::getPaginateMedias('kevin'); -$medias = $result['medias'] +## Installation -if($result['hasNextPage'] === true) { - $result = Instagram::getPaginateMedias('kevin', $result['maxId']); - $medias = array_merge($medias, $result['medias']); -} +### Using composer -echo json_encode($medias); +```sh +composer.phar require raiym/instagram-php-scraper ``` - -### Get media by code -```php -$media = Instagram::getMediaByCode('BDs9iwfL7XA'); +or +```sh +composer require raiym/instagram-php-scraper ``` -### Get media by url -```php -$media = Instagram::getMediaByUrl('https://www.instagram.com/p/BDs9iwfL7XA/'); -echo $media->owner->username; -``` - -### Get media by id -```php -$media = Instagram::getMediaById(1042815830884781756); -``` - -### Search medias by tag name -```php -$medias = Instagram::getMediasByTag('zara', 30); -echo json_encode($medias); -``` - -### Paginate medias by tag name -```php -$result = Instagram::getPaginateMediasByTag('zara'); -$medias = $result['medias'] - -if($result['hasNextPage'] === true) { - $result = Instagram::getPaginateMediasByTag('zara', $result['maxId']); - $medias = array_merge($medias, $result['medias']); -} - -echo json_encode($medias); -``` +### If you don't have composer +You can download it [here](https://getcomposer.org/download/). -### Get top medias by tag name -```php -$medias = Instagram::getTopMediasByTagName('durov'); -``` - -### Get media by id -```php -$media = Instagram::getMediaById(1270593720437182847) -``` - -### Convert media id to shortcode -```php -echo 'CODE: ' . Media::getCodeFromId('1270593720437182847_3'); -// OR -echo 'CODE: ' . Media::getCodeFromId('1270593720437182847'); -// OR -echo 'CODE: ' . Media::getCodeFromId(1270593720437182847); -// CODE: BGiDkHAgBF_ -// So you can do like this: instagram.com/p/BGiDkHAgBF_ -``` - -### Convert shortcode to media id -```php -echo 'Media id: ' . Media::getIdFromCode('BGiDkHAgBF_'); -// Media id: 1270593720437182847 -``` - -### Get media comments by shortcode -```php -$comments = Instagram::getMediaCommentsByCode('BG3Iz-No1IZ', 8000); -``` - -### Get media comments by id -```php -$comments = Instagram::getMediaCommentsById('1130748710921700586', 10000) -``` - -### Get location id -```php -$medias = Instagram::getLocationById(1); -``` - -### Get location top medias by location id -```php -$medias = Instagram::getLocationTopMediasById(1); -``` - -### Get location medias by location id -```php -$medias = Instagram::getLocationMediasById(1); -``` +## Examples +See examples [here](https://github.com/postaddictme/instagram-php-scraper/tree/master/examples). -### Other +## Other Java library: https://github.com/postaddictme/instagram-java-scraper \ No newline at end of file diff --git a/composer.json b/composer.json index 39b0cc64..fd98b466 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,8 @@ ], "require": { "php": ">=5.4.0", - "mashape/unirest-php": "3.0.*" + "mashape/unirest-php": "3.0.*", + "phpFastCache/phpFastCache": "5.0.*" }, "require-dev": { "phpunit/phpunit": "5.5.*" diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..df635b4e --- /dev/null +++ b/examples/README.md @@ -0,0 +1 @@ +# Examples diff --git a/examples/convertShortcode.php b/examples/convertShortcode.php new file mode 100644 index 00000000..f4347656 --- /dev/null +++ b/examples/convertShortcode.php @@ -0,0 +1,12 @@ +login(); +$account = $instagram->getAccountById('3'); + +// Available fields +echo "Account info:\n"; +echo "Id: {$account->getId()}\n"; +echo "Username: {$account->getUsername()}\n"; +echo "Full name: {$account->getFullName()}\n"; +echo "Biography: {$account->getBiography()}\n"; +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 "Is private: {$account->isPrivate()}\n"; +echo "Is verified: {$account->isVerified()}\n"; diff --git a/examples/getAccountByUsername.php b/examples/getAccountByUsername.php new file mode 100644 index 00000000..07633363 --- /dev/null +++ b/examples/getAccountByUsername.php @@ -0,0 +1,24 @@ +getAccount('kevin'); + +// Available fields +echo "Account info:\n"; +echo "Id: {$account->getId()}\n"; +echo "Username: {$account->getUsername()}\n"; +echo "Full name: {$account->getFullName()}\n"; +echo "Biography: {$account->getBiography()}\n"; +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 "Is private: {$account->isPrivate()}\n"; +echo "Is verified: {$account->isVerified()}\n"; diff --git a/examples/getAccountFollowers.php b/examples/getAccountFollowers.php new file mode 100644 index 00000000..9d880b7b --- /dev/null +++ b/examples/getAccountFollowers.php @@ -0,0 +1,13 @@ +login(); +sleep(2); // Delay to mimic user + +$username = 'kevin'; +$followers = []; +$account = $instagram->getAccount($username); +sleep(1); +$followers = $instagram->getFollowers($account->getId(), 1000, 100, true); // Get 1000 followers 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/getAccountMediasByUsername.php b/examples/getAccountMediasByUsername.php new file mode 100644 index 00000000..8410e8a1 --- /dev/null +++ b/examples/getAccountMediasByUsername.php @@ -0,0 +1,33 @@ +getMedias('kevin', 25); + +// Let's look at $media +$media = $medias[0]; + +echo "Media info:\n"; +echo "Id: {$media->getId()}\n"; +echo "Shotrcode: {$media->getShortCode()}\n"; +echo "Created at: {$media->getCreatedTime()}\n"; +echo "Caption: {$media->getCaption()}\n"; +echo "Number of comments: {$media->getCommentsCount()}"; +echo "Number of likes: {$media->getLikesCount()}"; +echo "Get link: {$media->getLink()}"; +echo "High resolution image: {$media->getImageHighResolutionUrl()}"; +echo "Media type (video or image): {$media->getType()}"; +$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"; + + +// If account private you should be subscribed and after auth it will be available +$instagram = \InstagramScraper\Instagram::withCredentials('username', 'password', 'path/to/cache/folder'); +$instagram->login(); +$medias = $instagram->getMedias('private_account', 100); diff --git a/examples/getCurrentTopMediasByLocationId.php b/examples/getCurrentTopMediasByLocationId.php new file mode 100644 index 00000000..7c1f1efd --- /dev/null +++ b/examples/getCurrentTopMediasByLocationId.php @@ -0,0 +1,24 @@ +login(); + +$medias = $instagram->getCurrentTopMediasByLocationId('1'); +$media = $medias[0]; +echo "Media info:\n"; +echo "Id: {$media->getId()}\n"; +echo "Shotrcode: {$media->getShortCode()}\n"; +echo "Created at: {$media->getCreatedTime()}\n"; +echo "Caption: {$media->getCaption()}\n"; +echo "Number of comments: {$media->getCommentsCount()}"; +echo "Number of likes: {$media->getLikesCount()}"; +echo "Get link: {$media->getLink()}"; +echo "High resolution image: {$media->getImageHighResolutionUrl()}"; +echo "Media type (video or image): {$media->getType()}"; +$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/getCurrentTopMediasByTagName.php b/examples/getCurrentTopMediasByTagName.php new file mode 100644 index 00000000..dfd83aa5 --- /dev/null +++ b/examples/getCurrentTopMediasByTagName.php @@ -0,0 +1,24 @@ +login(); + +$medias = $instagram->getCurrentTopMediasByTagName('youneverknow'); +$media = $medias[0]; +echo "Media info:\n"; +echo "Id: {$media->getId()}\n"; +echo "Shotrcode: {$media->getShortCode()}\n"; +echo "Created at: {$media->getCreatedTime()}\n"; +echo "Caption: {$media->getCaption()}\n"; +echo "Number of comments: {$media->getCommentsCount()}"; +echo "Number of likes: {$media->getLikesCount()}"; +echo "Get link: {$media->getLink()}"; +echo "High resolution image: {$media->getImageHighResolutionUrl()}"; +echo "Media type (video or image): {$media->getType()}"; +$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/getLocationById.php b/examples/getLocationById.php new file mode 100644 index 00000000..35fe2b26 --- /dev/null +++ b/examples/getLocationById.php @@ -0,0 +1,17 @@ +login(); + +// Location id from facebook +$location = $instagram->getLocationById(1); + +echo "Location info: \n"; +echo "Id: {$location->getId()}\n"; +echo "Name: {$location->getName()}\n"; +echo "Latitude: {$location->getLat()}\n"; +echo "Longitude: {$location->getLng()}\n"; +echo "Slug: {$location->getSlug()}\n"; +echo "Is public page available: {$location->getHasPublicPage()}\n"; + diff --git a/examples/getMediaByCode.php b/examples/getMediaByCode.php new file mode 100644 index 00000000..f56e75a0 --- /dev/null +++ b/examples/getMediaByCode.php @@ -0,0 +1,26 @@ +login(); + +$media = $instagram->getMediaByCode('BHaRdodBouH'); + +echo "Media info:\n"; +echo "Id: {$media->getId()}\n"; +echo "Shotrcode: {$media->getShortCode()}\n"; +echo "Created at: {$media->getCreatedTime()}\n"; +echo "Caption: {$media->getCaption()}\n"; +echo "Number of comments: {$media->getCommentsCount()}"; +echo "Number of likes: {$media->getLikesCount()}"; +echo "Get link: {$media->getLink()}"; +echo "High resolution image: {$media->getImageHighResolutionUrl()}"; +echo "Media type (video or image): {$media->getType()}"; +$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/getMediaById.php b/examples/getMediaById.php new file mode 100644 index 00000000..e5aa896c --- /dev/null +++ b/examples/getMediaById.php @@ -0,0 +1,27 @@ +login(); + +$media = $instagram->getMediaById('1270593720437182847'); +echo "Media info:\n"; +echo "Id: {$media->getId()}\n"; +echo "Shotrcode: {$media->getShortCode()}\n"; +echo "Created at: {$media->getCreatedTime()}\n"; +echo "Caption: {$media->getCaption()}\n"; +echo "Number of comments: {$media->getCommentsCount()}"; +echo "Number of likes: {$media->getLikesCount()}"; +echo "Get link: {$media->getLink()}"; +echo "High resolution image: {$media->getImageHighResolutionUrl()}"; +echo "Media type (video or image): {$media->getType()}"; +$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"; \ No newline at end of file diff --git a/examples/getMediaByUrl.php b/examples/getMediaByUrl.php new file mode 100644 index 00000000..e732ce96 --- /dev/null +++ b/examples/getMediaByUrl.php @@ -0,0 +1,27 @@ +login(); + +$media = $instagram->getMediaByUrl('https://www.instagram.com/p/BHaRdodBouH'); +echo "Media info:\n"; +echo "Id: {$media->getId()}\n"; +echo "Shotrcode: {$media->getShortCode()}\n"; +echo "Created at: {$media->getCreatedTime()}\n"; +echo "Caption: {$media->getCaption()}\n"; +echo "Number of comments: {$media->getCommentsCount()}"; +echo "Number of likes: {$media->getLikesCount()}"; +echo "Get link: {$media->getLink()}"; +echo "High resolution image: {$media->getImageHighResolutionUrl()}"; +echo "Media type (video or image): {$media->getType()}"; +$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"; \ No newline at end of file diff --git a/examples/getMediaComments.php b/examples/getMediaComments.php new file mode 100644 index 00000000..fb339795 --- /dev/null +++ b/examples/getMediaComments.php @@ -0,0 +1,32 @@ +login(); + +// Get media comments by shortcode +$comments = $instagram->getMediaCommentsByCode('BG3Iz-No1IZ', 8000); + +// or by id +$comments = $instagram->getMediaCommentsById('1130748710921700586', 10000); + +// Let's take first comment in array and explore available fields +$comment = $comments[0]; + +echo "Comment info: \n"; +echo "Id: {$comment->getId()}\n"; +echo "Created at: {$comment->getCreatedAt()}\n"; +echo "Comment text: {$comment->getText()}\n"; +$account = $comment->getOwner(); +echo "Comment owner: \n"; +echo "Id: {$account->getId()}"; +echo "Username: {$account->getUsername()}"; +echo "Profile picture url: {$account->getProfilePicUrl()}\n"; + +// You can start loading comments from specific comment by providing comment id +$comments = $instagram->getMediaCommentsByCode('BG3Iz-No1IZ', 200, $comment->getId()); + +// ... + + + diff --git a/examples/getMediasByLocationId.php b/examples/getMediasByLocationId.php new file mode 100644 index 00000000..ab2b9dc3 --- /dev/null +++ b/examples/getMediasByLocationId.php @@ -0,0 +1,24 @@ +login(); + +$medias = $instagram->getMediasByLocationId('1', 20); +$media = $medias[0]; +echo "Media info:\n"; +echo "Id: {$media->getId()}\n"; +echo "Shotrcode: {$media->getShortCode()}\n"; +echo "Created at: {$media->getCreatedTime()}\n"; +echo "Caption: {$media->getCaption()}\n"; +echo "Number of comments: {$media->getCommentsCount()}"; +echo "Number of likes: {$media->getLikesCount()}"; +echo "Get link: {$media->getLink()}"; +echo "High resolution image: {$media->getImageHighResolutionUrl()}"; +echo "Media type (video or image): {$media->getType()}"; +$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/getMediasByTag.php b/examples/getMediasByTag.php new file mode 100644 index 00000000..8f6afcf8 --- /dev/null +++ b/examples/getMediasByTag.php @@ -0,0 +1,24 @@ +login(); + +$medias = $instagram->getMediasByTag('youneverknow', 20); +$media = $medias[0]; +echo "Media info:\n"; +echo "Id: {$media->getId()}\n"; +echo "Shotrcode: {$media->getShortCode()}\n"; +echo "Created at: {$media->getCreatedTime()}\n"; +echo "Caption: {$media->getCaption()}\n"; +echo "Number of comments: {$media->getCommentsCount()}"; +echo "Number of likes: {$media->getLikesCount()}"; +echo "Get link: {$media->getLink()}"; +echo "High resolution image: {$media->getImageHighResolutionUrl()}"; +echo "Media type (video or image): {$media->getType()}"; +$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/getPaginateMediasByTag.php b/examples/getPaginateMediasByTag.php new file mode 100644 index 00000000..311d1d62 --- /dev/null +++ b/examples/getPaginateMediasByTag.php @@ -0,0 +1,15 @@ +login(); + +$result = $instagram->getPaginateMediasByTag('zara'); +$medias = $result['medias']; + +if ($result['hasNextPage'] === true) { + $result = $instagram->getPaginateMediasByTag('zara', $result['maxId']); + $medias = array_merge($medias, $result['medias']); +} + +echo json_encode($medias); \ No newline at end of file diff --git a/examples/paginateAccountMediaByUsername.php b/examples/paginateAccountMediaByUsername.php new file mode 100644 index 00000000..01518c44 --- /dev/null +++ b/examples/paginateAccountMediaByUsername.php @@ -0,0 +1,16 @@ +login(); + +$result = $instagram->getPaginateMedias('kevin'); +$medias = $result['medias']; +if ($result['hasNextPage'] === true) { + $result = $instagram->getPaginateMedias('kevin', $result['maxId']); + $medias = array_merge($medias, $result['medias']); +} + +echo json_encode($medias); \ No newline at end of file diff --git a/examples/searchAccountsByUsername.php b/examples/searchAccountsByUsername.php new file mode 100644 index 00000000..f57002e0 --- /dev/null +++ b/examples/searchAccountsByUsername.php @@ -0,0 +1,16 @@ +login(); + + +$accounts = $instagram->searchAccountsByUsername('raiym'); + +$account = $accounts[0]; +// Following fields are available in this request +echo "Account info:\n"; +echo "Username: {$account->getUsername()}"; +echo "Full name: {$account->getFullName()}"; +echo "Profile pic url: {$account->getProfilePicUrl()}"; + diff --git a/index.php b/index.php deleted file mode 100644 index 2f342aaf..00000000 --- a/index.php +++ /dev/null @@ -1,25 +0,0 @@ -'; -//echo Media::getCodeFromId('936303077400215759_123123'); -//echo Media::getLinkFromId('936303077400215759_123123'); -//echo shorten(936303077400215759); - -//echo json_encode($instagram->getMediaById('936303077400215759_123123123')); \ No newline at end of file diff --git a/src/InstagramScraper.php b/src/InstagramScraper.php index 4d882187..2d1870ae 100644 --- a/src/InstagramScraper.php +++ b/src/InstagramScraper.php @@ -1,11 +1,17 @@ $value) { + $url .= "&$key=$value"; + } + return $url; + } + public static function getFollowUrl($accountId) + { + $url = str_replace('{{accountId}}', urlencode($accountId), Endpoints::FOLLOW_URL); + return $url; + } + + public static function getFollowersJsonLink($accountId, $count, $after = '') + { + $url = str_replace('{{accountId}}', urlencode($accountId), Endpoints::FOLLOWERS_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; } } diff --git a/src/InstagramScraper/Exception/InstagramAuthException.php b/src/InstagramScraper/Exception/InstagramAuthException.php new file mode 100644 index 00000000..ca5fc77f --- /dev/null +++ b/src/InstagramScraper/Exception/InstagramAuthException.php @@ -0,0 +1,9 @@ +code === 404) { + if (is_null($sessionFolder)) { + $sessionFolder = __DIR__ . DIRECTORY_SEPARATOR . 'sessions' . DIRECTORY_SEPARATOR; + } + if (is_string($sessionFolder)) { + CacheManager::setDefaultConfig([ + 'path' => $sessionFolder, + ]); + self::$instanceCache = CacheManager::getInstance('files'); + } else { + self::$instanceCache = $sessionFolder; + } + $instance = new self(); + $instance->sessionUsername = $username; + $instance->sessionPassword = $password; + return $instance; + } + + /** + * @param string $tag + * + * @return array + * @throws InstagramException + * @throws InstagramNotFoundException + */ + public static function searchTagsByTagName($tag) + { + // TODO: Add tests and auth + $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) { throw new InstagramNotFoundException('Account with given username does not exist.'); } - if ($response->code !== 200) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . $response->body . ' Something went wrong. Please report issue.'); + // 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.'); } - $userArray = json_decode($response->raw_body, true); - if (!isset($userArray['user'])) { - throw new InstagramException('Account with this username does not exist'); + $jsonResponse = json_decode($response->raw_body, true); + if (!isset($jsonResponse['status']) || $jsonResponse['status'] != 'ok') { + throw new InstagramException('Response code is not equal 200. Something went wrong. Please report issue.'); } - return Account::fromAccountPage($userArray['user']); + + if (!isset($jsonResponse['hashtags']) || empty($jsonResponse['hashtags'])) { + return []; + } + $hashtags = []; + foreach ($jsonResponse['hashtags'] as $jsonHashtag) { + $hashtags[] = Tag::create($jsonHashtag['hashtag']); + } + return $hashtags; } - public static function getAccountById($id) + /** + * @param \stdClass|string $rawError + * + * @return string + */ + private static function getErrorBody($rawError) { - if (!is_numeric($id)) { - throw new \InvalidArgumentException('User id must be integer or integer wrapped in string'); - } - $parameters = Endpoints::getAccountJsonInfoLinkByAccountId($id); - $userArray = json_decode(self::getContentsFromUrl($parameters), true); - if ($userArray['status'] === 'fail') { - throw new InstagramException($userArray['message']); + if (is_string($rawError)) { + return $rawError; } - if (!isset($userArray['username'])) { - throw new InstagramNotFoundException('User with this id not found'); + if (is_object($rawError)) { + $str = ""; + foreach ($rawError as $key => $value) { + $str .= " " . $key . " => " . $value . ";"; + } + return $str; + } else { + return "Unknown body format"; } - return Account::fromAccountPage($userArray); + } - private static function getContentsFromUrl($parameters) + /** + * @param string $username + * + * @return Account[] + * @throws InstagramException + * @throws InstagramNotFoundException + */ + public function searchAccountsByUsername($username) { - if (!function_exists('curl_init')) { - return false; + $response = Request::get(Endpoints::getGeneralSearchJsonLink($username), $this->generateHeaders($this->userSession)); + if (self::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.'); + } + + $jsonResponse = json_decode($response->raw_body, true); + 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'])) { + return []; } - $random = self::generateRandomString(); - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, Endpoints::INSTAGRAM_QUERY_URL); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, 'q=' . $parameters); - $headers = array(); - $headers[] = "Cookie: csrftoken=$random;"; - $headers[] = "X-Csrftoken: $random"; - $headers[] = "Referer: https://www.instagram.com/"; - curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); - $output = curl_exec($ch); - curl_close($ch); - return $output; + + $accounts = []; + foreach ($jsonResponse['users'] as $jsonAccount) { + $accounts[] = Account::create($jsonAccount['user']); + } + return $accounts; } - private static function generateRandomString($length = 10) + /** + * @param $session + * + * @return array + */ + private function generateHeaders($session) { - $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; - $charactersLength = strlen($characters); - $randomString = ''; - for ($i = 0; $i < $length; $i++) { - $randomString .= $characters[rand(0, $charactersLength - 1)]; + $headers = []; + if ($session) { + $cookies = ''; + foreach ($session as $key => $value) { + $cookies .= "$key=$value; "; + } + $headers = [ + 'cookie' => $cookies, + 'referer' => Endpoints::BASE_URL . '/', + 'x-csrftoken' => $session['csrftoken'], + ]; } - return $randomString; + return $headers; } - public static function getMedias($username, $count = 20, $maxId = '') + /** + * @param string $username + * @param int $count + * @param string $maxId + * + * @return Media[] + * @throws InstagramException + */ + public function getMedias($username, $count = 20, $maxId = '') { $index = 0; $medias = []; $isMoreAvailable = true; while ($index < $count && $isMoreAvailable) { - $response = Request::get(Endpoints::getAccountMediasJsonLink($username, $maxId)); - if ($response->code !== 200) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . $response->body . ' Something went wrong. Please report issue.'); + $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: ' . $response->body . ' Something went wrong. Please report issue.'); + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } - if (count($arr['items']) === 0) { + // 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::fromApi($mediaArray); + $medias[] = Media::create($mediaArray); $index++; } - if (count($arr['items']) == 0) { + if (empty($arr['items']) || !isset($arr['items'])) { return $medias; } $maxId = $arr['items'][count($arr['items']) - 1]['id']; @@ -114,7 +206,70 @@ public static function getMedias($username, $count = 20, $maxId = '') return $medias; } - public static function getPaginateMedias($username, $maxId = '') + /** + * @param $mediaId + * + * @return Media + */ + public function getMediaById($mediaId) + { + $mediaLink = Media::getLinkFromId($mediaId); + return $this->getMediaByUrl($mediaLink); + } + + /** + * @param string $mediaUrl + * + * @return Media + * @throws InstagramException + * @throws InstagramNotFoundException + */ + public function getMediaByUrl($mediaUrl) + { + if (filter_var($mediaUrl, FILTER_VALIDATE_URL) === false) { + throw new \InvalidArgumentException('Malformed media url'); + } + $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) { + 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.'); + } + $mediaArray = json_decode($response->raw_body, true); + if (!isset($mediaArray['graphql']['shortcode_media'])) { + throw new InstagramException('Media with this code does not exist'); + } + return Media::create($mediaArray['graphql']['shortcode_media']); + } + + /** + * @param string $mediaCode (for example BHaRdodBouH) + * + * @return Media + * @throws InstagramException + * @throws InstagramNotFoundException + */ + + public function getMediaByCode($mediaCode) + { + $url = Endpoints::getMediaPageLink($mediaCode); + return $this->getMediaByUrl($url); + + } + + /** + * @param string $username + * @param string $maxId + * + * @return array + * @throws InstagramException + */ + public function getPaginateMedias($username, $maxId = '') { $hasNextPage = true; $medias = []; @@ -122,27 +277,33 @@ public static function getPaginateMedias($username, $maxId = '') $toReturn = [ 'medias' => $medias, 'maxId' => $maxId, - 'hasNextPage' => $hasNextPage + 'hasNextPage' => $hasNextPage, ]; - $response = Request::get(Endpoints::getAccountMediasJsonLink($username, $maxId)); + $response = Request::get(Endpoints::getAccountMediasJsonLink($username, $maxId), + $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.'); + // 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: ' . $response->body . ' Something went wrong. Please report issue.'); + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } - if (count($arr['items']) === 0) { + //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::fromApi($mediaArray); + $medias[] = Media::create($mediaArray); } $maxId = $arr['items'][count($arr['items']) - 1]['id']; @@ -151,53 +312,195 @@ public static function getPaginateMedias($username, $maxId = '') $toReturn = [ 'medias' => $medias, 'maxId' => $maxId, - 'hasNextPage' => $hasNextPage + 'hasNextPage' => $hasNextPage, ]; return $toReturn; } - public static function getMediaByCode($mediaCode) + /** + * @param $mediaId + * @param int $count + * @param null $maxId + * + * @return Comment[] + */ + public function getMediaCommentsById($mediaId, $count = 10, $maxId = null) { - return self::getMediaByUrl(Endpoints::getMediaPageLink($mediaCode)); + $code = Media::getCodeFromId($mediaId); + return self::getMediaCommentsByCode($code, $count, $maxId); } - public static function getMediaByUrl($mediaUrl) + /** + * @param $code + * @param int $count + * @param null $maxId + * + * @return Comment[] + * @throws InstagramException + */ + public function getMediaCommentsByCode($code, $count = 10, $maxId = null) { - if (filter_var($mediaUrl, FILTER_VALIDATE_URL) === false) { - throw new \InvalidArgumentException('Malformed media url'); + $remain = $count; + $comments = []; + $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; + } else { + $numberOfCommentsToRetreive = $remain; + $index += $remain; + $remain = 0; + } + if (!isset($maxId)) { + $maxId = ''; + + } + $commentsUrl = Endpoints::getCommentsBeforeCommentIdByCode($code, $numberOfCommentsToRetreive, $maxId); + $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.'); + } + $cookies = self::parseCookies($response->headers['Set-Cookie']); + $this->userSession['csrftoken'] = $cookies['csrftoken']; + $jsonResponse = json_decode($response->raw_body, true); + $nodes = $jsonResponse['data']['shortcode_media']['edge_media_to_comment']['edges']; + foreach ($nodes as $commentArray) { + $comments[] = Comment::create($commentArray['node']); + } + $hasPrevious = $jsonResponse['data']['shortcode_media']['edge_media_to_comment']['page_info']['has_next_page']; + $numberOfComments = $jsonResponse['data']['shortcode_media']['edge_media_to_comment']['count']; + if ($count > $numberOfComments) { + $count = $numberOfComments; + } + if (sizeof($nodes) == 0) { + return $comments; + } + $maxId = $nodes[sizeof($nodes) - 1]['node']['id']; } - $response = Request::get(rtrim($mediaUrl, '/') . '/?__a=1'); - if ($response->code === 404) { - throw new InstagramNotFoundException('Media with given code does not exist or account is private.'); + return $comments; + } + + /** + * @param string $rawCookies + * + * @return array + */ + private static function parseCookies($rawCookies) + { + if (!is_array($rawCookies)) { + $rawCookies = [$rawCookies]; } - if ($response->code !== 200) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . $response->body . ' Something went wrong. Please report issue.'); + + $cookies = []; + foreach ($rawCookies as $c) { + $c = explode(';', $c)[0]; + $parts = explode('=', $c); + if (sizeof($parts) >= 2 && !is_null($parts[1])) { + $cookies[$parts[0]] = $parts[1]; + } } - $mediaArray = json_decode($response->raw_body, true); - if (!isset($mediaArray['media'])) { - throw new InstagramException('Media with this code does not exist'); + return $cookies; + } + + /** + * @param string $id + * + * @return Account + * @throws InstagramException + */ + public function getAccountById($id) + { + // Use the follow page to get the account. The follow url will redirect to the home page for the user, + // which has the username embedded in the url. + + if (!is_numeric($id)) { + throw new \InvalidArgumentException('User id must be integer or integer wrapped in string'); + } + + $url = Endpoints::getFollowUrl($id); + + // Cut a request by disabling redirects. + Request::curlOpt(CURLOPT_FOLLOWLOCATION, FALSE); + $response = Request::get($url, $this->generateHeaders($this->userSession)); + Request::curlOpt(CURLOPT_FOLLOWLOCATION, TRUE); + + if ($response->code === 400) { + throw new InstagramException('Account with this id does not exist.'); + } + + if ($response->code !== 302) { + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->raw_body) . ' Something went wrong. Please report issue.'); } - return Media::fromMediaPage($mediaArray['media']); + + $cookies = self::parseCookies($response->headers['Set-Cookie']); + $this->userSession['csrftoken'] = $cookies['csrftoken']; + + // Get the username from the response url. + $responseUrl = $response->headers['Location']; + $urlParts = explode('/', rtrim($responseUrl, '/')); + $username = end($urlParts); + + return $this->getAccount($username); } - public static function getMediasByTag($tag, $count = 12, $maxId = '') + /** + * @param string $username + * + * @return Account + * @throws InstagramException + * @throws InstagramNotFoundException + */ + public function getAccount($username) + { + $response = Request::get(Endpoints::getAccountJsonLink($username), $this->generateHeaders($this->userSession)); + if (self::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.'); + } + + $userArray = json_decode($response->raw_body, true); + if (!isset($userArray['user'])) { + throw new InstagramException('Account with this username does not exist'); + } + return Account::create($userArray['user']); + } + + /** + * @param string $tag + * @param int $count + * @param string $maxId + * @param string $minTimestamp + * + * @return Media[] + * @throws InstagramException + */ + public function getMediasByTag($tag, $count = 12, $maxId = '', $minTimestamp = null) { $index = 0; $medias = []; $mediaIds = []; $hasNextPage = true; while ($index < $count && $hasNextPage) { - $response = Request::get(Endpoints::getMediasJsonByTagLink($tag, $maxId)); + $response = Request::get(Endpoints::getMediasJsonByTagLink($tag, $maxId), + $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.'); + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } - + $cookies = self::parseCookies($response->headers['Set-Cookie']); + $this->userSession['csrftoken'] = $cookies['csrftoken']; $arr = json_decode($response->raw_body, true); if (!is_array($arr)) { throw new InstagramException('Response decoding failed. Returned data corrupted or this library outdated. Please report issue'); } - if (count($arr['tag']['media']['count']) === 0) { + if (empty($arr['tag']['media']['count'])) { return []; } $nodes = $arr['tag']['media']['nodes']; @@ -205,15 +508,18 @@ public static function getMediasByTag($tag, $count = 12, $maxId = '') if ($index === $count) { return $medias; } - $media = Media::fromTagPage($mediaArray); - if (in_array($media->id, $mediaIds)) { + $media = Media::create($mediaArray); + if (in_array($media->getId(), $mediaIds)) { + return $medias; + } + if (isset($minTimestamp) && $media->getCreatedTime() < $minTimestamp) { return $medias; } - $mediaIds[] = $media->id; + $mediaIds[] = $media->getId(); $medias[] = $media; $index++; } - if (count($nodes) == 0) { + if (empty($nodes)) { return $medias; } $maxId = $arr['tag']['media']['page_info']['end_cursor']; @@ -222,7 +528,14 @@ public static function getMediasByTag($tag, $count = 12, $maxId = '') return $medias; } - public static function getPaginateMediasByTag($tag, $maxId = '') + /** + * @param string $tag + * @param string $maxId + * + * @return array + * @throws InstagramException + */ + public function getPaginateMediasByTag($tag, $maxId = '') { $hasNextPage = true; $medias = []; @@ -230,33 +543,37 @@ public static function getPaginateMediasByTag($tag, $maxId = '') $toReturn = [ 'medias' => $medias, 'maxId' => $maxId, - 'hasNextPage' => $hasNextPage + 'hasNextPage' => $hasNextPage, ]; - $response = Request::get(Endpoints::getMediasJsonByTagLink($tag, $maxId)); + $response = Request::get(Endpoints::getMediasJsonByTagLink($tag, $maxId), + $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.'); + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } + $cookies = self::parseCookies($response->headers['Set-Cookie']); + $this->userSession['csrftoken'] = $cookies['csrftoken']; + $arr = json_decode($response->raw_body, true); if (!is_array($arr)) { throw new InstagramException('Response decoding failed. Returned data corrupted or this library outdated. Please report issue'); } - if (count($arr['tag']['media']['count']) === 0) { + if (empty($arr['tag']['media']['count'])) { return $toReturn; } $nodes = $arr['tag']['media']['nodes']; - if (count($nodes) == 0) { + if (empty($nodes)) { return $toReturn; } foreach ($nodes as $mediaArray) { - $medias[] = Media::fromTagPage($mediaArray); + $medias[] = Media::create($mediaArray); } $maxId = $arr['tag']['media']['page_info']['end_cursor']; @@ -273,163 +590,92 @@ public static function getPaginateMediasByTag($tag, $maxId = '') return $toReturn; } - public static function searchAccountsByUsername($username) - { - $response = Request::get(Endpoints::getGeneralSearchJsonLink($username)); - if ($response->code === 404) { - throw new InstagramNotFoundException('Account with given username does not exist.'); - } - if ($response->code !== 200) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . $response->body . ' Something went wrong. Please report issue.'); - } - - $jsonResponse = json_decode($response->raw_body, true); - 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']) || count($jsonResponse['users']) == 0) { - return []; - } - - $accounts = []; - foreach ($jsonResponse['users'] as $jsonAccount) { - $accounts[] = Account::fromSearchPage($jsonAccount['user']); - } - return $accounts; - } - - public static function searchTagsByTagName($tag) - { - $response = Request::get(Endpoints::getGeneralSearchJsonLink($tag)); - if ($response->code === 404) { - throw new InstagramNotFoundException('Account with given username does not exist.'); - } - if ($response->code !== 200) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . $response->body . ' Something went wrong. Please report issue.'); - } - - $jsonResponse = json_decode($response->raw_body, true); - 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['hashtags']) || count($jsonResponse['hashtags']) == 0) { - return []; - } - $hashtags = []; - foreach ($jsonResponse['hashtags'] as $jsonHashtag) { - $hashtags[] = Tag::fromSearchPage($jsonHashtag['hashtag']); - } - return $hashtags; - } - - public static function getTopMediasByTagName($tagName) + /** + * @param $tagName + * + * @return Media[] + * @throws InstagramException + * @throws InstagramNotFoundException + */ + public function getCurrentTopMediasByTagName($tagName) { - $response = Request::get(Endpoints::getMediasJsonByTagLink($tagName, '')); + $response = Request::get(Endpoints::getMediasJsonByTagLink($tagName, ''), + $this->generateHeaders($this->userSession)); if ($response->code === 404) { throw new InstagramNotFoundException('Account with given username does not exist.'); } if ($response->code !== 200) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . $response->body . ' Something went wrong. Please report issue.'); + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($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); $medias = []; foreach ($jsonResponse['tag']['top_posts']['nodes'] as $mediaArray) { - $medias[] = Media::fromTagPage($mediaArray); + $medias[] = Media::create($mediaArray); } return $medias; } - public static function getMediaById($mediaId) - { - $mediaLink = Media::getLinkFromId($mediaId); - return self::getMediaByUrl($mediaLink); - } - - public static function getMediaCommentsById($mediaId, $count = 10, $maxId = null) - { - $code = Media::getCodeFromId($mediaId); - return self::getMediaCommentsByCode($code, $count, $maxId); - } - - public static function getMediaCommentsByCode($code, $count = 10, $maxId = null) - { - $remain = $count; - $comments = []; - $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; - } else { - $numberOfCommentsToRetreive = $remain; - $index += $remain; - $remain = 0; - } - if (!isset($maxId)) { - $parameters = Endpoints::getLastCommentsByCodeLink($code, $numberOfCommentsToRetreive); - - } else { - $parameters = Endpoints::getCommentsBeforeCommentIdByCode($code, $numberOfCommentsToRetreive, $maxId); - } - $jsonResponse = json_decode(self::getContentsFromUrl($parameters), true); - $nodes = $jsonResponse['comments']['nodes']; - foreach ($nodes as $commentArray) { - $comments[] = Comment::fromApi($commentArray); - } - $hasPrevious = $jsonResponse['comments']['page_info']['has_previous_page']; - $numberOfComments = $jsonResponse['comments']['count']; - if ($count > $numberOfComments) { - $count = $numberOfComments; - } - if (sizeof($nodes) == 0) { - return $comments; - } - $maxId = $nodes[sizeof($nodes) - 1]['id']; - } - return $comments; - } - - public static function getLocationTopMediasById($facebookLocationId) + /** + * @param $facebookLocationId + * + * @return Media[] + * @throws InstagramException + * @throws InstagramNotFoundException + */ + public function getCurrentTopMediasByLocationId($facebookLocationId) { - $response = Request::get(Endpoints::getMediasJsonByLocationIdLink($facebookLocationId)); + $response = Request::get(Endpoints::getMediasJsonByLocationIdLink($facebookLocationId), + $this->generateHeaders($this->userSession)); if ($response->code === 404) { throw new InstagramNotFoundException('Location with this id doesn\'t exist'); } if ($response->code !== 200) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . $response->body . ' Something went wrong. Please report issue.'); + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($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); $nodes = $jsonResponse['location']['top_posts']['nodes']; $medias = []; foreach ($nodes as $mediaArray) { - $medias[] = Media::fromTagPage($mediaArray); + $medias[] = Media::create($mediaArray); } return $medias; } - public static function getLocationMediasById($facebookLocationId, $quantity = 12, $offset = '') + /** + * @param string $facebookLocationId + * @param int $quantity + * @param string $offset + * + * @return Media[] + * @throws InstagramException + */ + public function getMediasByLocationId($facebookLocationId, $quantity = 12, $offset = '') { $index = 0; $medias = []; $hasNext = true; while ($index < $quantity && $hasNext) { - $response = Request::get(Endpoints::getMediasJsonByLocationIdLink($facebookLocationId, $offset)); + $response = Request::get(Endpoints::getMediasJsonByLocationIdLink($facebookLocationId, $offset), + $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.'); + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($response->body) . ' Something went wrong. Please report issue.'); } + $cookies = self::parseCookies($response->headers['Set-Cookie']); + $this->userSession['csrftoken'] = $cookies['csrftoken']; $arr = json_decode($response->raw_body, true); $nodes = $arr['location']['media']['nodes']; foreach ($nodes as $mediaArray) { if ($index === $quantity) { return $medias; } - $medias[] = Media::fromTagPage($mediaArray); + $medias[] = Media::create($mediaArray); $index++; } - if (count($nodes) == 0) { + if (empty($nodes)) { return $medias; } $hasNext = $arr['location']['media']['page_info']['has_next_page']; @@ -438,16 +684,185 @@ public static function getLocationMediasById($facebookLocationId, $quantity = 12 return $medias; } - public static function getLocationById($facebookLocationId) + /** + * @param string $facebookLocationId + * + * @return Location + * @throws InstagramException + * @throws InstagramNotFoundException + */ + public function getLocationById($facebookLocationId) { - $response = Request::get(Endpoints::getMediasJsonByLocationIdLink($facebookLocationId)); + $response = Request::get(Endpoints::getMediasJsonByLocationIdLink($facebookLocationId), + $this->generateHeaders($this->userSession)); if ($response->code === 404) { throw new InstagramNotFoundException('Location with this id doesn\'t exist'); } if ($response->code !== 200) { - throw new InstagramException('Response code is ' . $response->code . '. Body: ' . $response->body . ' Something went wrong. Please report issue.'); + throw new InstagramException('Response code is ' . $response->code . '. Body: ' . Instagram::getErrorBody($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); - return Location::makeLocation($jsonResponse['location']); + return Location::create($jsonResponse['location']); + } + + /** + * @param string $accountId Account id of the profile to query + * @param int $count Total followers 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 getFollowers($accountId, $count = 20, $pageSize = 20, $delayed = true) + { + if ($delayed) { + set_time_limit(1800); // 30 mins + } + + $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::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.'); + } + + $jsonResponse = json_decode($response->raw_body, true); + + if ($jsonResponse['data']['user']['edge_followed_by']['count'] === 0) { + return $accounts; + } + + $edgesArray = $jsonResponse['data']['user']['edge_followed_by']['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_followed_by']['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(1000000, 3000000); + usleep($microsec); + } + } + return $accounts; } -} \ No newline at end of file + + /** + * @param bool $force + * + * @throws InstagramAuthException + * @throws InstagramException + * + * @return array + */ + public function login($force = false) + { + if ($this->sessionUsername == null || $this->sessionPassword == null) { + throw new InstagramAuthException("User credentials not provided"); + } + + $cachedString = self::$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.'); + } + $cookies = self::parseCookies($response->headers['Set-Cookie']); + $mid = $cookies['mid']; + $csrfToken = $cookies['csrftoken']; + $headers = ['cookie' => "csrftoken=$csrfToken; mid=$mid;", + 'referer' => Endpoints::BASE_URL . '/', + 'x-csrftoken' => $csrfToken, + ]; + $response = Request::post(Endpoints::LOGIN_URL, $headers, + ['username' => $this->sessionUsername, 'password' => $this->sessionPassword]); + + if ($response->code !== 200) { + if ((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.'); + } + } + + if (is_object($response->body)) { + if (!$response->body->authenticated) { + throw new InstagramAuthException('User credentials are wrong.'); + } + } + + $cookies = self::parseCookies($response->headers['Set-Cookie']); + $cookies['mid'] = $mid; + $cachedString->set($cookies); + self::$instanceCache->save($cachedString); + $this->userSession = $cookies; + } else { + $this->userSession = $session; + } + + return $this->generateHeaders($this->userSession); + } + + /** + * @param $session + * + * @return bool + */ + public function isLoggedIn($session) + { + if (is_null($session) || !isset($session['sessionid'])) { + return false; + } + $sessionId = $session['sessionid']; + $csrfToken = $session['csrftoken']; + $headers = ['cookie' => "csrftoken=$csrfToken; sessionid=$sessionId;", + 'referer' => Endpoints::BASE_URL . '/', + 'x-csrftoken' => $csrfToken, + ]; + $response = Request::get(Endpoints::BASE_URL, $headers); + if ($response->code !== 200) { + return false; + } + $cookies = self::parseCookies($response->headers['Set-Cookie']); + if (!isset($cookies['ds_user_id'])) { + return false; + } + return true; + } + + /** + * + */ + public function saveSession() + { + $cachedString = self::$instanceCache->getItem($this->sessionUsername); + $cachedString->set($this->userSession); + } + +} diff --git a/src/InstagramScraper/InstagramQueryId.php b/src/InstagramScraper/InstagramQueryId.php new file mode 100644 index 00000000..4ee5d2b0 --- /dev/null +++ b/src/InstagramScraper/InstagramQueryId.php @@ -0,0 +1,13 @@ +isLoaded; + } + + /** + * @return string + */ + public function getUsername() + { + return $this->username; + } + + /** + * @return int + */ + public function getId() + { + return (int)$this->id; + } + + /** + * @return string + */ + public function getFullName() + { + return $this->fullName; + } + + /** + * @return string + */ + public function getProfilePicUrl() + { + return $this->profilePicUrl; + } + + /** + * @return string + */ + public function getBiography() + { + return $this->biography; + } + + /** + * @return string + */ + public function getExternalUrl() + { + return $this->externalUrl; + } + + /** + * @return int + */ + public function getFollowsCount() + { + return $this->followsCount; + } + + /** + * @return int + */ + public function getFollowedByCount() + { + return $this->followedByCount; + } + + /** + * @return int + */ + public function getMediaCount() { + return $this->mediaCount; } - public static function fromAccountPage($userArray) + /** + * @return bool + */ + public function isPrivate() { - $instance = new self(); - $instance->username = $userArray['username']; - $instance->followsCount = $userArray['follows']['count']; - $instance->followedByCount = $userArray['followed_by']['count']; - $instance->profilePicUrl = $userArray['profile_pic_url']; - $instance->id = $userArray['id']; - $instance->biography = $userArray['biography']; - $instance->fullName = $userArray['full_name']; - $instance->mediaCount = $userArray['media']['count']; - $instance->isPrivate = $userArray['is_private']; - $instance->externalUrl = $userArray['external_url']; - $instance->isVerified = $userArray['is_verified']; - return $instance; + return $this->isPrivate; } - public static function fromMediaPage($userArray) + /** + * @return bool + */ + public function isVerified() { - $instance = new self(); - $instance->username = $userArray['username']; - $instance->profilePicUrl = $userArray['profile_pic_url']; - $instance->id = $userArray['id']; - $instance->fullName = $userArray['full_name']; - $instance->isPrivate = $userArray['is_private']; - return $instance; + return $this->isVerified; } - public static function fromSearchPage($userArray) + /** + * @param $value + * @param $prop + * @param $array + */ + protected function initPropertiesCustom($value, $prop, $array) { - $instance = new self(); - $instance->username = $userArray['username']; - $instance->profilePicUrl = $userArray['profile_pic_url']; - $instance->id = $userArray['pk']; - $instance->fullName = $userArray['full_name']; - $instance->isPrivate = $userArray['is_private']; - $instance->isVerified = $userArray['is_verified']; - $instance->followedByCount = $userArray['follower_count']; - return $instance; + switch ($prop) { + case 'id': + $this->id = (int)$value; + break; + case 'username': + $this->username = $value; + break; + case 'full_name': + $this->fullName = $value; + break; + case 'profile_pic_url': + $this->profilePicUrl = $value; + break; + case 'biography': + $this->biography = $value; + break; + case 'external_url': + $this->externalUrl = $value; + break; + case 'follows': + $this->followsCount = !empty($array[$prop]['count']) ? (int)$array[$prop]['count'] : 0; + break; + case 'followed_by': + $this->followedByCount = !empty($array[$prop]['count']) ? (int)$array[$prop]['count'] : 0; + break; + case 'media': + $this->mediaCount = !empty($array[$prop]['count']) ? $array[$prop]['count'] : 0; + break; + case 'is_private': + $this->isPrivate = (bool)$value; + break; + case 'is_verified': + $this->isVerified = (bool)$value; + break; + } } } \ No newline at end of file diff --git a/src/InstagramScraper/Model/CarouselMedia.php b/src/InstagramScraper/Model/CarouselMedia.php new file mode 100644 index 00000000..0e487161 --- /dev/null +++ b/src/InstagramScraper/Model/CarouselMedia.php @@ -0,0 +1,235 @@ +type; + } + + /** + * @param mixed $type + * + * @return $this + */ + public function setType($type) + { + $this->type = $type; + return $this; + } + + /** + * @return mixed + */ + public function getImageLowResolutionUrl() + { + return $this->imageLowResolutionUrl; + } + + /** + * @param mixed $imageLowResolutionUrl + * + * @return CarouselMedia + */ + public function setImageLowResolutionUrl($imageLowResolutionUrl) + { + $this->imageLowResolutionUrl = $imageLowResolutionUrl; + return $this; + } + + /** + * @return mixed + */ + public function getImageThumbnailUrl() + { + return $this->imageThumbnailUrl; + } + + /** + * @param mixed $imageThumbnailUrl + * + * @return CarouselMedia + */ + public function setImageThumbnailUrl($imageThumbnailUrl) + { + $this->imageThumbnailUrl = $imageThumbnailUrl; + return $this; + } + + /** + * @return mixed + */ + public function getImageStandardResolutionUrl() + { + return $this->imageStandardResolutionUrl; + } + + /** + * @param mixed $imageStandardResolutionUrl + * + * @return CarouselMedia + */ + public function setImageStandardResolutionUrl($imageStandardResolutionUrl) + { + $this->imageStandardResolutionUrl = $imageStandardResolutionUrl; + return $this; + } + + /** + * @return mixed + */ + public function getImageHighResolutionUrl() + { + return $this->imageHighResolutionUrl; + } + + /** + * @param mixed $imageHighResolutionUrl + * + * @return CarouselMedia + */ + public function setImageHighResolutionUrl($imageHighResolutionUrl) + { + $this->imageHighResolutionUrl = $imageHighResolutionUrl; + return $this; + } + + /** + * @return mixed + */ + public function getVideoLowResolutionUrl() + { + return $this->videoLowResolutionUrl; + } + + /** + * @param mixed $videoLowResolutionUrl + * + * @return CarouselMedia + */ + public function setVideoLowResolutionUrl($videoLowResolutionUrl) + { + $this->videoLowResolutionUrl = $videoLowResolutionUrl; + return $this; + } + + /** + * @return mixed + */ + public function getVideoStandardResolutionUrl() + { + return $this->videoStandardResolutionUrl; + } + + /** + * @param mixed $videoStandardResolutionUrl + * + * @return CarouselMedia + */ + public function setVideoStandardResolutionUrl($videoStandardResolutionUrl) + { + $this->videoStandardResolutionUrl = $videoStandardResolutionUrl; + return $this; + } + + /** + * @return mixed + */ + public function getVideoLowBandwidthUrl() + { + return $this->videoLowBandwidthUrl; + } + + /** + * @param mixed $videoLowBandwidthUrl + * + * @return CarouselMedia + */ + public function setVideoLowBandwidthUrl($videoLowBandwidthUrl) + { + $this->videoLowBandwidthUrl = $videoLowBandwidthUrl; + return $this; + } + + /** + * @return mixed + */ + public function getVideoViews() + { + return $this->videoViews; + } + + /** + * @param mixed $videoViews + * + * @return CarouselMedia + */ + public function setVideoViews($videoViews) + { + $this->videoViews = $videoViews; + return $this; + } + +} diff --git a/src/InstagramScraper/Model/Comment.php b/src/InstagramScraper/Model/Comment.php index da725df7..a39a5e1d 100644 --- a/src/InstagramScraper/Model/Comment.php +++ b/src/InstagramScraper/Model/Comment.php @@ -3,26 +3,85 @@ namespace InstagramScraper\Model; -class Comment +class Comment extends AbstractModel { - public $text; - public $createdAt; - public $id; + /** + * @var + */ + protected $id; - public $user; + /** + * @var + */ + protected $text; - function __construct() + /** + * @var + */ + protected $createdAt; + + /** + * @var Account + */ + protected $owner; + + /** + * @var bool + */ + protected $isLoaded = false; + + /** + * @return mixed + */ + public function getId() + { + return $this->id; + } + + /** + * @return mixed + */ + public function getText() + { + return $this->text; + } + + /** + * @return mixed + */ + public function getCreatedAt() + { + return $this->createdAt; + } + + /** + * @return Account + */ + public function getOwner() { + return $this->owner; } - public static function fromApi($commentArray) + /** + * @param $value + * @param $prop + */ + protected function initPropertiesCustom($value, $prop) { - $instance = new self(); - $instance->text = $commentArray['text']; - $instance->createdAt = $commentArray['created_at']; - $instance->id = $commentArray['id']; - $instance->user = Account::fromAccountPage($commentArray['user']); - return $instance; + switch ($prop) { + case 'id': + $this->id = $value; + break; + case 'created_at': + $this->createdAt = $value; + break; + case 'text': + $this->text = $value; + break; + case 'owner': + $this->owner = Account::create($value); + break; + } } } \ No newline at end of file diff --git a/src/InstagramScraper/Model/Location.php b/src/InstagramScraper/Model/Location.php index 4366f216..1178c57a 100644 --- a/src/InstagramScraper/Model/Location.php +++ b/src/InstagramScraper/Model/Location.php @@ -3,24 +3,93 @@ namespace InstagramScraper\Model; -class Location +class Location extends AbstractModel { - public $id; - public $name; - public $lat; - public $lng; + /** + * @var array + */ + protected static $initPropertiesMap = [ + 'id' => 'id', + 'has_public_page' => 'hasPublicPage', + 'name' => 'name', + 'slug' => 'slug', + 'lat' => 'lat', + 'lng' => 'lng', + ]; + /** + * @var + */ + protected $id; + /** + * @var + */ + protected $hasPublicPage; + /** + * @var + */ + protected $name; + /** + * @var + */ + protected $slug; + /** + * @var + */ + protected $lng; + /** + * @var + */ + protected $lat; + /** + * @var bool + */ + protected $isLoaded = false; - function __construct() + /** + * @return mixed + */ + public function getId() { + return $this->id; } - public static function makeLocation($locationArray) + /** + * @return mixed + */ + public function getHasPublicPage() { - $location = new Location(); - $location->id = $locationArray['id']; - $location->name = $locationArray['name']; - $location->lat = $locationArray['lat']; - $location->lng = $locationArray['lng']; - return $location; + return $this->hasPublicPage; + } + + /** + * @return mixed + */ + public function getName() + { + return $this->name; + } + + /** + * @return mixed + */ + public function getSlug() + { + return $this->slug; + } + + /** + * @return mixed + */ + public function getLng() + { + return $this->lng; + } + + /** + * @return mixed + */ + public function getLat() + { + return $this->lat; } } \ No newline at end of file diff --git a/src/InstagramScraper/Model/Media.php b/src/InstagramScraper/Model/Media.php index 089f0427..4da84499 100644 --- a/src/InstagramScraper/Model/Media.php +++ b/src/InstagramScraper/Model/Media.php @@ -4,150 +4,137 @@ use InstagramScraper\Endpoints; -class Media +/** + * Class Media + * @package InstagramScraper\Model + */ +class Media extends AbstractModel { - public $id; - public $createdTime; - public $type; - public $link; - public $imageLowResolutionUrl; - public $imageThumbnailUrl; - public $imageStandardResolutionUrl; - public $imageHighResolutionUrl; - public $caption; - public $captionIsEdited; - public $isAd; - public $videoLowResolutionUrl; - public $videoStandardResolutionUrl; - public $videoLowBandwidthUrl; - public $videoViews; - public $code; - public $owner; - public $ownerId; - public $likesCount; - public $locationId; - public $locationName; - public $commentsCount; - - function __construct() - { - } - - public static function fromApi($mediaArray) - { - $instance = new self(); - $instance->id = $mediaArray['id']; - $instance->type = $mediaArray['type']; - $instance->createdTime = $mediaArray['created_time']; - $instance->code = $mediaArray['code']; - $instance->link = $mediaArray['link']; - $instance->commentsCount = $mediaArray['comments']['count']; - $instance->likesCount = $mediaArray['likes']['count']; - $images = self::getImageUrls($mediaArray['images']['standard_resolution']['url']); - $instance->imageLowResolutionUrl = $images['low']; - $instance->imageThumbnailUrl = $images['thumbnail']; - $instance->imageStandardResolutionUrl = $images['standard']; - $instance->imageHighResolutionUrl = $images['high']; - if (isset($mediaArray['caption'])) { - $instance->caption = $mediaArray['caption']['text']; - } - if ($instance->type === 'video') { - if (isset($mediaArray['video_views'])) { - $instance->videoViews = $mediaArray['video_views']; - } - if (isset($mediaArray['videos'])) { - $instance->videoLowResolutionUrl = $mediaArray['videos']['low_resolution']['url']; - $instance->videoStandardResolutionUrl = $mediaArray['videos']['standard_resolution']['url']; - $instance->videoLowBandwidthUrl = $mediaArray['videos']['low_bandwidth']['url']; - } - } - if (isset($mediaArray['location']['id'])) { - $instance->locationId = $mediaArray['location']['id']; - } - if (isset($mediaArray['location']['name'])) { - $instance->locationName = $mediaArray['location']['name']; - } - return $instance; - } + const TYPE_IMAGE = 'image'; + const TYPE_VIDEO = 'video'; + const TYPE_SIDECAR = 'sidecar'; + const TYPE_CAROUSEL = 'carousel'; - private static function getImageUrls($imageUrl) - { - $parts = explode('/', parse_url($imageUrl)['path']); - $imageName = $parts[sizeof($parts) - 1]; - $urls = [ - 'thumbnail' => Endpoints::INSTAGRAM_CDN_URL . 't/s150x150/' . $imageName, - 'low' => Endpoints::INSTAGRAM_CDN_URL . 't/s320x320/' . $imageName, - 'standard' => Endpoints::INSTAGRAM_CDN_URL . 't/s640x640/' . $imageName, - 'high' => Endpoints::INSTAGRAM_CDN_URL . 't/' . $imageName - ]; - return $urls; - } + /** + * @var string + */ + protected $id = ''; - public static function fromMediaPage($mediaArray) - { - $instance = new self(); - $instance->id = $mediaArray['id']; - $instance->type = 'image'; - if ($mediaArray['is_video']) { - $instance->type = 'video'; - $instance->videoStandardResolutionUrl = $mediaArray['video_url']; - $instance->videoViews = $mediaArray['video_views']; - } - if (isset($mediaArray['caption_is_edited'])) { - $instance->captionIsEdited = $mediaArray['caption_is_edited']; - } - if (isset($mediaArray['is_ad'])) { - $instance->isAd = $mediaArray['is_ad']; - } - $instance->createdTime = $mediaArray['date']; - $instance->code = $mediaArray['code']; - $instance->link = Endpoints::getMediaPageLink($instance->code); - $instance->commentsCount = $mediaArray['comments']['count']; - $instance->likesCount = $mediaArray['likes']['count']; - $images = self::getImageUrls($mediaArray['display_src']); - $instance->imageStandardResolutionUrl = $images['standard']; - $instance->imageLowResolutionUrl = $images['low']; - $instance->imageHighResolutionUrl = $images['high']; - $instance->imageThumbnailUrl = $images['thumbnail']; - if (isset($mediaArray['caption'])) { - $instance->caption = $mediaArray['caption']; - } - if (isset($mediaArray['location']['id'])) { - $instance->locationId = $mediaArray['location']['id']; - } - if (isset($mediaArray['location']['name'])) { - $instance->locationName = $mediaArray['location']['name']; - } - $instance->owner = Account::fromMediaPage($mediaArray['owner']); - return $instance; - } + /** + * @var string + */ + protected $shortCode = ''; - public static function fromTagPage($mediaArray) - { - $instance = new self(); - $instance->code = $mediaArray['code']; - $instance->link = Endpoints::getMediaPageLink($instance->code); - $instance->commentsCount = $mediaArray['comments']['count']; - $instance->likesCount = $mediaArray['likes']['count']; - $instance->ownerId = $mediaArray['owner']['id']; - if (isset($mediaArray['caption'])) { - $instance->caption = $mediaArray['caption']; - } - $instance->createdTime = $mediaArray['date']; - $images = self::getImageUrls($mediaArray['display_src']); - $instance->imageStandardResolutionUrl = $images['standard']; - $instance->imageLowResolutionUrl = $images['low']; - $instance->imageHighResolutionUrl = $images['high']; - $instance->imageThumbnailUrl = $images['thumbnail']; - $instance->type = 'image'; - if ($mediaArray['is_video']) { - $instance->type = 'video'; - $instance->videoViews = $mediaArray['video_views']; - } - $instance->id = $mediaArray['id']; - return $instance; - } + /** + * @var int + */ + protected $createdTime = 0; + + /** + * @var string + */ + protected $type = ''; + + /** + * @var string + */ + protected $link = ''; + + /** + * @var string + */ + protected $imageLowResolutionUrl = ''; + + /** + * @var string + */ + protected $imageThumbnailUrl = ''; + + /** + * @var string + */ + protected $imageStandardResolutionUrl = ''; + + /** + * @var string + */ + protected $imageHighResolutionUrl = ''; + /** + * @var array + */ + protected $carouselMedia = []; + + /** + * @var string + */ + protected $caption = ''; + + /** + * @var bool + */ + protected $isCaptionEdited = false; + + /** + * @var bool + */ + protected $isAd = false; + + /** + * @var string + */ + protected $videoLowResolutionUrl = ''; + + /** + * @var string + */ + protected $videoStandardResolutionUrl = ''; + + /** + * @var string + */ + protected $videoLowBandwidthUrl = ''; + + /** + * @var int + */ + protected $videoViews = 0; + + /** + * @var Account + */ + protected $owner; + + /** + * @var int + */ + protected $ownerId = 0; + + /** + * @var int + */ + protected $likesCount = 0; + + /** + * @var + */ + protected $locationId; + + /** + * @var string + */ + protected $locationName = ''; + + /** + * @var string + */ + protected $commentsCount = 0; + + /** + * @param string $code + * + * @return int + */ public static function getIdFromCode($code) { $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; @@ -159,12 +146,22 @@ public static function getIdFromCode($code) return $id; } + /** + * @param string $id + * + * @return mixed + */ public static function getLinkFromId($id) { $code = Media::getCodeFromId($id); return Endpoints::getMediaPageLink($code); } + /** + * @param string $id + * + * @return string + */ public static function getCodeFromId($id) { $parts = explode('_', $id); @@ -178,4 +175,363 @@ public static function getCodeFromId($id) }; return $code; } -} \ No newline at end of file + + /** + * @return mixed + */ + public function getId() + { + return $this->id; + } + + /** + * @return string + */ + public function getShortCode() + { + return $this->shortCode; + } + + /** + * @return int + */ + public function getCreatedTime() + { + return $this->createdTime; + } + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @return string + */ + public function getLink() + { + return $this->link; + } + + /** + * @return string + */ + public function getImageLowResolutionUrl() + { + return $this->imageLowResolutionUrl; + } + + /** + * @return string + */ + public function getImageThumbnailUrl() + { + return $this->imageThumbnailUrl; + } + + /** + * @return string + */ + public function getImageStandardResolutionUrl() + { + return $this->imageStandardResolutionUrl; + } + + /** + * @return string + */ + public function getImageHighResolutionUrl() + { + return $this->imageHighResolutionUrl; + } + + /** + * @return array + */ + public function getCarouselMedia() + { + return $this->carouselMedia; + } + + /** + * @return string + */ + public function getCaption() + { + return $this->caption; + } + + /** + * @return bool + */ + public function isCaptionEdited() + { + return $this->isCaptionEdited; + } + + /** + * @return bool + */ + public function isAd() + { + return $this->isAd; + } + + /** + * @return string + */ + public function getVideoLowResolutionUrl() + { + return $this->videoLowResolutionUrl; + } + + /** + * @return string + */ + public function getVideoStandardResolutionUrl() + { + return $this->videoStandardResolutionUrl; + } + + /** + * @return string + */ + public function getVideoLowBandwidthUrl() + { + return $this->videoLowBandwidthUrl; + } + + /** + * @return int + */ + public function getVideoViews() + { + return $this->videoViews; + } + + /** + * @return int + */ + public function getOwnerId() + { + return $this->ownerId; + } + + /** + * @return int + */ + public function getLikesCount() + { + return $this->likesCount; + } + + /** + * @return mixed + */ + public function getLocationId() + { + return $this->locationId; + } + + /** + * @return string + */ + public function getLocationName() + { + return $this->locationName; + } + + /** + * @return string + */ + public function getCommentsCount() + { + return $this->commentsCount; + } + + /** + * @param $value + * @param $prop + */ + protected function initPropertiesCustom($value, $prop, $arr) + { + switch ($prop) { + case 'id': + $this->id = $value; + break; + case 'type': + $this->type = $value; + break; + case 'created_time': + $this->createdTime = (int)$value; + break; + case 'code': + $this->shortCode = $value; + break; + case 'link': + $this->link = $value; + break; + case 'comments': + $this->commentsCount = $arr[$prop]['count']; + break; + case 'likes': + $this->likesCount = $arr[$prop]['count']; + break; + case 'images': + $images = self::getImageUrls($arr[$prop]['standard_resolution']['url']); + $this->imageLowResolutionUrl = $images['low']; + $this->imageThumbnailUrl = $images['thumbnail']; + $this->imageStandardResolutionUrl = $images['standard']; + $this->imageHighResolutionUrl = $images['high']; + break; + case 'carousel_media': + $this->type = self::TYPE_CAROUSEL; + $this->carouselMedia = []; + foreach ($arr["carousel_media"] as $carouselArray) { + self::setCarouselMedia($arr, $carouselArray, $this); + } + break; + case 'caption': + $this->caption = $arr[$prop]; + break; + case 'video_views': + $this->videoViews = $value; + 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; + } + $this->locationName = $arr[$prop]['name']; + break; + case 'user': + $this->owner = Account::create($arr[$prop]); + break; + case 'is_video': + $this->type = self::TYPE_VIDEO; + break; + case 'video_url': + $this->videoStandardResolutionUrl = $value; + break; + case 'video_view_count': + $this->videoViews = $value; + break; + case 'caption_is_edited': + $this->isCaptionEdited = $value; + break; + case 'is_ad': + $this->isAd = $value; + break; + case 'taken_at_timestamp': + $this->createdTime = $value; + break; + case 'shortcode': + $this->shortCode = $value; + $this->link = Endpoints::getMediaPageLink($this->shortCode); + break; + case 'edge_media_to_comment': + $this->commentsCount = $arr[$prop]['count']; + $this->likesCount = $arr[$prop]['count']; + break; + case 'display_url': + $images = self::getImageUrls($arr[$prop]); + $this->imageStandardResolutionUrl = $images['standard']; + $this->imageLowResolutionUrl = $images['low']; + $this->imageHighResolutionUrl = $images['high']; + $this->imageThumbnailUrl = $images['thumbnail']; + break; + case 'edge_media_to_caption': + $this->caption = $arr[$prop]['edges'][0]['node']['text']; + break; + case 'owner': + $this->owner = Account::create($arr[$prop]); + break; + case 'date': + $this->createdTime = (int)$value; + break; + case 'display_src': + $images = self::getImageUrls($value); + $this->imageStandardResolutionUrl = $images['standard']; + $this->imageLowResolutionUrl = $images['low']; + $this->imageHighResolutionUrl = $images['high']; + $this->imageThumbnailUrl = $images['thumbnail']; + $this->type = self::TYPE_IMAGE; + break; + } + if (!$this->ownerId && !is_null($this->owner)) { + $this->ownerId = $this->getOwner()->getId(); + } + } + + /** + * @param string $imageUrl + * + * @return array + */ + private static function getImageUrls($imageUrl) + { + $parts = explode('/', parse_url($imageUrl)['path']); + $imageName = $parts[sizeof($parts) - 1]; + $urls = [ + 'thumbnail' => Endpoints::INSTAGRAM_CDN_URL . 't/s150x150/' . $imageName, + 'low' => Endpoints::INSTAGRAM_CDN_URL . 't/s320x320/' . $imageName, + 'standard' => Endpoints::INSTAGRAM_CDN_URL . 't/s640x640/' . $imageName, + 'high' => Endpoints::INSTAGRAM_CDN_URL . 't/' . $imageName, + ]; + return $urls; + } + + /** + * @param $mediaArray + * @param $carouselArray + * @param $instance + * + * @return mixed + */ + private static function setCarouselMedia($mediaArray, $carouselArray, $instance) + { + $carouselMedia = new CarouselMedia(); + $carouselMedia->setType($carouselArray['type']); + + if (isset($carouselArray['images'])) { + $carouselImages = self::getImageUrls($carouselArray['images']['standard_resolution']['url']); + $carouselMedia->setImageLowResolutionUrl($carouselImages['low']); + $carouselMedia->setImageThumbnailUrl($carouselImages['thumbnail']); + $carouselMedia->setImageStandardResolutionUrl($carouselImages['standard']); + $carouselMedia->setImageHighResolutionUrl($carouselImages['high']); + } + + if ($carouselMedia->getType() === self::TYPE_VIDEO) { + if (isset($mediaArray['video_views'])) { + $carouselMedia->setVideoViews($carouselArray['video_views']); + } + if (isset($carouselArray['videos'])) { + $carouselMedia->setVideoLowResolutionUrl($carouselArray['videos']['low_resolution']['url']); + $carouselMedia->setVideoStandardResolutionUrl($carouselArray['videos']['standard_resolution']['url']); + $carouselMedia->setVideoLowBandwidthUrl($carouselArray['videos']['low_bandwidth']['url']); + } + } + array_push($instance->carouselMedia, $carouselMedia); + return $mediaArray; + } + + /** + * @return Account + */ + public function getOwner() + { + return $this->owner; + } +} diff --git a/src/InstagramScraper/Model/Tag.php b/src/InstagramScraper/Model/Tag.php index da98738e..4215edc3 100644 --- a/src/InstagramScraper/Model/Tag.php +++ b/src/InstagramScraper/Model/Tag.php @@ -2,23 +2,54 @@ namespace InstagramScraper\Model; - -class Tag +/** + * Class Tag + * @package InstagramScraper\Model + */ +class Tag extends AbstractModel { - public $mediaCount; - public $name; - public $id; + /** + * @var array + */ + protected static $initPropertiesMap = [ + 'media_count' => 'mediaCount', + 'name' => 'name', + 'id' => 'initInt', + ]; + /** + * @var int + */ + protected $mediaCount = 0; + /** + * @var string + */ + protected $name; + /** + * @var int + */ + protected $id; + + /** + * @return int + */ + public function getMediaCount() + { + return $this->mediaCount; + } - function __construct() + /** + * @return string + */ + public function getName() { + return $this->name; } - public static function fromSearchPage($tagArray) + /** + * @return int + */ + public function getId() { - $instance = new self(); - $instance->mediaCount = $tagArray['media_count']; - $instance->name = $tagArray['name']; - $instance->id = $tagArray['id']; - return $instance; + return $this->id; } } \ No newline at end of file diff --git a/src/InstagramScraper/Traits/ArrayLikeTrait.php b/src/InstagramScraper/Traits/ArrayLikeTrait.php new file mode 100644 index 00000000..78cf8c3d --- /dev/null +++ b/src/InstagramScraper/Traits/ArrayLikeTrait.php @@ -0,0 +1,109 @@ +isMethod($offset, 'get') || \property_exists($this, $offset); + } + + /** + * @param mixed $offset + * + * @return mixed + */ + 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; + } + } + + /** + * @param mixed $offset + * @param mixed $value + * + * @return void + */ + public function offsetSet($offset, $value) + { + if ($run = $this->isMethod($offset, 'set')) { + $this->run($run); + } else { + $this->{$offset} = $value; + } + } + + /** + * @param mixed $offset + * + * @return void + */ + public function offsetUnset($offset) + { + if ($run = $this->isMethod($offset, 'unset')) { + $this->run($run); + } else { + $this->{$offset} = null; + } + } + + /** + * @param $method + * @param $case + * + * @return bool|string + */ + protected function isMethod($method, $case) + { + $uMethod = $case . \ucfirst($method); + if (\method_exists($this, $uMethod)) { + return $uMethod; + } + if (\method_exists($this, $method)) { + return $method; + } + return false; + } + + /** + * @param $method + * + * @return mixed + */ + protected function run($method) + { + if (\is_array($method)) { + $params = $method; + $method = \array_shift($params); + if ($params) { + return \call_user_func_array([$this, $method], $params); + } + } + 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 new file mode 100644 index 00000000..482d7c4d --- /dev/null +++ b/src/InstagramScraper/Traits/InitializerTrait.php @@ -0,0 +1,334 @@ +setFake(true); + } + + /** + * @param array $props + */ + protected function __construct(array $props = null) + { + $this->beforeInit(); + $this->modified = \time(); + if ($this->isAutoConstruct) { + $this->initAuto(); + } elseif (empty($props)) { + $this->initDefaults(); + } else { + $this->init($props); + } + $this->afterInit(); + } + + /** + * @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; + } + + /** + * @param bool $value + * + * @return $this + */ + protected function setFake($value = true) + { + $this->isFake = (bool) $value; + + return $this; + } + + /** + * @param array $props + * + * @return $this + */ + final protected function init(array $props) + { + //?reflection? + foreach ($props as $prop => $value) { + if (\method_exists($this, 'initPropertiesCustom')) { + \call_user_func([$this, 'initPropertiesCustom'], $value, $prop, $props); + } elseif (isset(static::$initPropertiesMap[$prop])) { + $methodOrProp = static::$initPropertiesMap[$prop]; + if (\method_exists($this, $methodOrProp)) { + //if there is method then use it firstly + \call_user_func([$this, $methodOrProp], $value, $prop, $props); + } elseif (\property_exists($this, $methodOrProp)) { + //if there is property then it just assign value + $this->{$methodOrProp} = $value; + } else { + //otherwise fill help data array + //for following initialization + $this->data[$methodOrProp] = $value; + $this->data[$prop] = $value; + } + } else { + //otherwise fill help data array + $this->data[$prop] = $value; + } + } + $this->isNew = false; + $this->isLoaded = true; + $this->isLoadEmpty = false; + + return $this; + } + + /** + * @return $this + */ + final protected function initAuto() + { + 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() + { + return $this; + } + + /** + * @return $this + */ + protected function beforeInit() + { + return $this; + } + + /** + * @return $this + */ + protected function afterInit() + { + return $this; + } + + /** + * @param $datetime + * + * @return $this + */ + protected function initModified($datetime) + { + $this->modified = \strtotime($datetime); + + return $this; + } + + /** + * @param string $date + * @param string $key + * + * @return $this + */ + protected function initDatetime($date, $key) + { + return $this->initProperty(\strtotime($date), $key); + } + + /** + * @param mixed $value + * @param string $key + * + * @return $this + */ + protected function initBool($value, $key) + { + return $this->initProperty(!empty($value), "is{$key}", $key); + } + + /** + * @param mixed $value + * @param string $key + * + * @return $this + */ + protected function initInt($value, $key) + { + return $this->initProperty((int) $value, $key); + } + + /** + * @param mixed $value + * @param string $key + * + * @return $this + */ + protected function initFloat($value, $key) + { + return $this->initProperty((float) $value, $key); + } + + /** + * @param string $rawData + * @param string $key + * + * @return $this + */ + protected function initJsonArray($rawData, $key) + { + $value = \json_decode($rawData, true); + if (empty($value)) { + //could not resolve - + if ('null' === $rawData or '' === $rawData) { + $value = []; + } else { + $value = (array) $rawData; + } + } else { + $value = (array) $value; + } + + return $this->initProperty($value, $key); + } + + /** + * @param mixed $value + * @param string $key + * + * @return $this + */ + 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 fa13e404..1fe22ed4 100644 --- a/tests/InstagramTest.php +++ b/tests/InstagramTest.php @@ -4,65 +4,100 @@ use InstagramScraper\Instagram; use InstagramScraper\Model\Media; +use phpFastCache\CacheManager; use PHPUnit\Framework\TestCase; class InstagramTest extends TestCase { + private static $instagram; + + public static function setUpBeforeClass() + { + $sessionFolder = __DIR__ . DIRECTORY_SEPARATOR . 'sessions' . DIRECTORY_SEPARATOR; + CacheManager::setDefaultConfig([ + 'path' => $sessionFolder + ]); + $instanceCache = CacheManager::getInstance('files'); + self::$instagram = Instagram::withCredentials('raiym', 'uvebzdxgbkt2T5_K', $instanceCache); + self::$instagram->login(); + + } + public function testGetAccountByUsername() { - $account = Instagram::getAccount('kevin'); - $this->assertEquals('kevin', $account->username); - $this->assertEquals('3', $account->id); + $account = self::$instagram->getAccount('kevin'); + $this->assertEquals('kevin', $account->getUsername()); + $this->assertEquals('3', $account->getId()); } public function testGetAccountById() { - $account = Instagram::getAccountById(3); - $this->assertEquals('kevin', $account->username); - $this->assertEquals('3', $account->id); + + $account = self::$instagram->getAccountById(3); + $this->assertEquals('kevin', $account->getUsername()); + $this->assertEquals('3', $account->getId()); + } + + public function testGetAccountByIdWithInvalidNumericId() + { + // PHP_INT_MAX is far larger than the greatest id so far and thus does not represent a valid account. + $this->expectException(\InstagramScraper\Exception\InstagramException::class); + self::$instagram->getAccountById(PHP_INT_MAX); } public function testGetMedias() { - $medias = Instagram::getMedias('kevin', 80); + $medias = self::$instagram->getMedias('kevin', 80); $this->assertEquals(80, sizeof($medias)); } - public function testGet1000Medias() + public function testGet100Medias() { - $medias = Instagram::getMedias('kevin', 1000); - $this->assertEquals(1000, sizeof($medias)); + $medias = self::$instagram->getMedias('kevin', 100); + $this->assertEquals(100, sizeof($medias)); + } + + public function testGetMediasByTag() + { + $medias = self::$instagram->getMediasByTag('youneverknow', 20); + $this->assertEquals(20, sizeof($medias)); } public function testGetMediaByCode() { - $media = Instagram::getMediaByCode('BHaRdodBouH'); - $this->assertEquals('kevin', $media->owner->username); + $media = self::$instagram->getMediaByCode('BHaRdodBouH'); + $this->assertEquals('kevin', $media->getOwner()->getUsername()); } public function testGetMediaByUrl() { - $media = Instagram::getMediaByUrl('https://www.instagram.com/p/BHaRdodBouH'); - $this->assertEquals('kevin', $media->owner->username); + $media = self::$instagram->getMediaByUrl('https://www.instagram.com/p/BHaRdodBouH'); + $this->assertEquals('kevin', $media->getOwner()->getUsername()); } public function testGetLocationTopMediasById() { - $medias = Instagram::getLocationTopMediasById(1); + $medias = self::$instagram->getLocationTopMediasById(1); $this->assertEquals(9, count($medias)); } public function testGetLocationMediasById() { - $medias = Instagram::getLocationMediasById(1); + $medias = self::$instagram->getLocationMediasById(1); $this->assertEquals(12, count($medias)); } public function testGetLocationById() { - $location = Instagram::getLocationById(1); - $this->assertEquals('Dog Patch Labs', $location->name); + $location = self::$instagram->getLocationById(1); + $this->assertEquals('Dog Patch Labs', $location->getName()); + } + + public function testGetMediaByTag() + { + $medias = self::$instagram->getTopMediasByTagName('hello'); + echo json_encode($medias); } public function testGetIdFromCode() @@ -80,4 +115,14 @@ public function testGetCodeFromId() $id = Media::getIdFromCode('BGiDkHAgBF_'); $this->assertEquals(1270593720437182847, $id); } + + public function testGeMediaCommentsByCode() + { + $comments = self::$instagram->getMediaCommentsByCode('BR5Njq1gKmB', 40); + //TODO: check why returns less comments + $this->assertEquals(32, sizeof($comments)); + } + + // TODO: Add test getMediaById + // TODO: Add test getLocationById } \ No newline at end of file