diff --git a/.gitignore b/.gitignore index be4b633..c51f2c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,12 @@ +.idea vendor build .php_cs.cache +/cache +!/cache/.gitkeep +/tests/cache/ +!/tests/cache/empty/.gitkeep +!/tests/cache/invalid/.gitkeep +!/tests/cache/invalid/demo.cache +!/tests/cache/valid/.gitkeep +!/tests/cache/valid/pgrimaud.cache \ No newline at end of file diff --git a/README.md b/README.md index 1feaa78..556f17f 100644 --- a/README.md +++ b/README.md @@ -6,44 +6,60 @@ [![Test Coverage](https://codeclimate.com/github/pgrimaud/instagram-user-feed/badges/coverage.svg)](https://codeclimate.com/github/pgrimaud/instagram-user-feed/coverage) [![Issue Count](https://codeclimate.com/github/pgrimaud/instagram-user-feed/badges/issue_count.svg)](https://codeclimate.com/github/pgrimaud/instagram-user-feed) -## Installation +## Information +This library offers 2 packages to retrieve your or any Instagram feed without oAuth for PHP. -``` -composer require pgrimaud/instagram-user-feed -``` +## Version ^4.0 +This version can retrieve **YOUR** Instagram feed using an **access token**. -1. Visit [http://instagram.pixelunion.net/](http://instagram.pixelunion.net/) and create an access token +- [Installation](#installation-of-version-40) +- [Usage](#usage-of-version-40) +- [Paginate](#paginate-for-version-40) -2. The first part of the access token is your User Id +## Version ^5.0 +This version can retrieve **ANY** Instagram feed using **web scrapping**. -``` -$api = new Api(); +- [Installation](#installation-of-version-50) +- [Usage](#usage-of-version-50) +- [Paginate](#paginate-for-version-50) -$api->setAccessToken('1234578.abcabc.abcabcabcabcabcabcabcabcabcabc'); -$api->setUserId(1234578); -``` +## Changelog -Seems like you can only access your own media until 2020 other user's media December 11, 2018. Hope to find a solution for long term. +**2018-04-20 : Release of version ^5.0 in parallel of version ^4.0 which still working. (Kudos for [@jannejava](https://github.com/jannejava) and [@cookieguru](https://github.com/cookieguru)** -## Warning +~~2018-04-17 : Now fetching data with screen scraping (thanks [@cookieguru](https://github.com/cookieguru)), please upgrade to version ^5.0~~ -**2018-04-16 : Now fetching data with access token, only for your account (thanks [@jannejava](https://github.com/jannejava)), please upgrade to version ^4.0** +~~2018-04-16 : Now fetching data with access token, only for your account (thanks [@jannejava](https://github.com/jannejava)), please upgrade to version ^4.0~~ ~~2018-04-08 : Due to changes of the Instagram API (again...), you must upgrade to version ^3.0~~ ~~2018-03-16 : Due to changes of the Instagram API, you must upgrade to version ^2.1~~ -## Usage +# Installation of version ^4.0 -### Retrieve data +``` +composer require pgrimaud/instagram-user-feed "^4.0" +``` -```php -$api = new Api(); +1. Visit [http://instagram.pixelunion.net/](http://instagram.pixelunion.net/) and create an access token + +2. The first part of the access token is your User Id + +``` +$api = new \Instagram\Api(); -$api->setUserId(184263228); $api->setAccessToken('1234578.abcabc.abcabcabcabcabcabcabcabcabcabc'); +$api->setUserId(1234578); +``` -$feed = $api->getFeed(); +**Seems like you can only access your own media until 2020.** + +## Usage of version ^4.0 + +```php +$api = new \Instagram\Api(); + +$feed = $api->getFeed('pgrimaud'); print_r($feed); @@ -90,7 +106,7 @@ Instagram\Hydrator\Feed Object ) ``` -### Paginate +## Paginate for version ^4.0 If you want to use paginate, retrieve `maxId` from previous call and add it to your next call. ```php @@ -115,5 +131,90 @@ $feed = $api->getFeed(); // And etc... ``` +___ + +# Installation of version ^5.0 + +``` +composer require pgrimaud/instagram-user-feed "^5.0" +``` + +## Usage of version ^5.0 +```php +$cache = new Instagram\Storage\CacheManager(); +$api = new Instagram\Api($cache); +$api->setUserName('pgrimaud'); + +$feed = $api->getFeed(); + +print_r($feed): + +``` +```php +Instagram\Hydrator\Component\Feed Object +( + [id] => 184263228 + [userName] => pgrimaud + [fullName] => Pierre G + [biography] => Gladiator retired - ESGI 14' + [followers] => 342 + [following] => 114 + [profilePicture] => https://scontent-cdg2-1.cdninstagram.com/vp/f49bc1ac9af43314d3354b4c4a987c6d/5B5BB12E/t51.2885-19/10483606_1498368640396196_604136733_a.jpg + [externalUrl] => https://p.ier.re/ + [mediaCount] => 33 + [medias] => Array + ( + [0] => Instagram\Hydrator\Component\Media Object + ( + [id] => 1758133053345287778 + [typeName] => GraphImage + [height] => 1080 + [width] => 1080 + [thumbnailSrc] => https://scontent-cdg2-1.cdninstagram.com/vp/dd39e08d3c740e764c61bc694d36f5a7/5B643B2F/t51.2885-15/s640x640/sh0.08/e35/30604700_183885172242354_7971196573931536384_n.jpg + [link] => https://www.instagram.com/p/BhmJLJwhM5i/ + [date] => DateTime Object + ( + [date] => 2018-04-15 17:23:33.000000 + [timezone_type] => 3 + [timezone] => Europe/Paris + ) + + [displaySrc] => https://scontent-cdg2-1.cdninstagram.com/vp/51a54157b8868d715b8dd51a5ecbc46d/5B632D4E/t51.2885-15/e35/30604700_183885172242354_7971196573931536384_n.jpg + [caption] => + [comments] => 2 + [likes] => 14 + ) + + ) + + ... + + [endCursor] => AQBkklLNRIkvdOUFDHvLEZrssIcYn2TauR6cpvDgxiGJZq8mHb8ZFWNVwql1W78We0aOgfJZyQDF32yoP_h2zRKZ2iRY6zVJdDaLaGfUU23iXA +) + +``` + +## Paginate for version ^5.0 +If you want to use paginate, retrieve `endCursor` from previous call and add it to your next call. + +```php +// Initialization + +$cache = new Instagram\Storage\CacheManager(); +$api = new Instagram\Api($cache); +$api->setUserName('pgrimaud'); + +// First call : + +$feed = $api->getFeed(); + +// Second call : + +$endCursor = $feed->getEndCursor(); +$api->setEndCursor($endCursor); +$feed = $api->getFeed(); + +// And etc... +``` diff --git a/cache/.gitkeep b/cache/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/composer.json b/composer.json index 2fac643..aad93d7 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,10 @@ { "name": "Charles Salvan", "email": "charles.salvan@hotmail.fr" + }, + { + "name": "Tim Bond", + "email": "cookieguru@gmail.com" } ], "autoload": { diff --git a/examples/medias.php b/examples/medias.php new file mode 100644 index 0000000..be9647e --- /dev/null +++ b/examples/medias.php @@ -0,0 +1,69 @@ +setUserName('pgrimaud'); + +try { + // First page + + /** @var \Instagram\Hydrator\Component\Feed $feed */ + $feed = $api->getFeed(); + + echo '============================' . "\n"; + echo 'User Informations : ' . "\n"; + echo '============================' . "\n\n"; + + echo 'ID : ' . $feed->getId() . "\n"; + echo 'Full Name : ' . $feed->getFullName() . "\n"; + echo 'UserName : ' . $feed->getUserName() . "\n"; + echo 'Following : ' . $feed->getFollowing() . "\n"; + echo 'Followers : ' . $feed->getFollowers() . "\n\n"; + + echo '============================' . "\n"; + echo 'Medias first page : ' . "\n"; + echo '============================' . "\n\n"; + + /** @var \Instagram\Hydrator\Component\Media $media */ + foreach ($feed->getMedias() as $media) { + echo 'ID : ' . $media->getId() . "\n"; + echo 'Caption : ' . $media->getCaption() . "\n"; + echo 'Link : ' . $media->getLink() . "\n"; + echo 'Likes : ' . $media->getLikes() . "\n"; + echo 'Date : ' . $media->getDate()->format('Y-m-d h:i:s') . "\n"; + echo '============================' . "\n"; + } + + // Second Page + + $api->setEndCursor($feed->getEndCursor()); + + sleep(1); // avoir 429 Rate limit from Instagram + + $feed = $api->getFeed(); + + echo "\n\n"; + echo '============================' . "\n"; + echo 'Medias second page : ' . "\n"; + echo '============================' . "\n\n"; + + /** @var \Instagram\Hydrator\Component\Media $media */ + foreach ($feed->getMedias() as $media) { + echo 'ID : ' . $media->getId() . "\n"; + echo 'Caption : ' . $media->getCaption() . "\n"; + echo 'Link : ' . $media->getLink() . "\n"; + echo 'Likes : ' . $media->getLikes() . "\n"; + echo 'Date : ' . $media->getDate()->format('Y-m-d h:i:s') . "\n"; + echo '============================' . "\n"; + } + + // And etc... + +} Catch (\Instagram\Exception\InstagramException $exception) { + print_r($exception->getMessage()); +} + +// Second page \ No newline at end of file diff --git a/src/Instagram/Api.php b/src/Instagram/Api.php index 39e6229..71694fc 100644 --- a/src/Instagram/Api.php +++ b/src/Instagram/Api.php @@ -4,94 +4,83 @@ use GuzzleHttp\Client; use Instagram\Exception\InstagramException; -use Instagram\Transport\JsonFeed; +use Instagram\Hydrator\HtmlHydrator; +use Instagram\Hydrator\JsonHydrator; +use Instagram\Storage\CacheManager; +use Instagram\Transport\HtmlTransportFeed; +use Instagram\Transport\JsonTransportFeed; class Api { /** - * @var Client + * @var CacheManager */ - private $clientUser = null; + private $cacheManager; /** * @var Client */ - private $clientMedia = null; - - /** - * @var integer - */ - private $userId = null; + private $client = null; /** * @var string */ - private $accessToken = null; + private $userName; /** * @var string */ - private $maxId = null; + private $endCursor = null; /** * Api constructor. - * @param Client|null $clientUser - * @param Client|null $clientMedia - */ - public function __construct(Client $clientUser = null, Client $clientMedia = null) - { - $this->clientUser = $clientUser ?: new Client(); - $this->clientMedia = $clientMedia ?: new Client(); - } - - /** - * @param int $userId - */ - public function setUserId($userId) - { - $this->userId = $userId; - } - - /** - * @param $token + * @param Client|null $client + * @param CacheManager|null $cacheManager */ - public function setAccessToken($token) + public function __construct(CacheManager $cacheManager, Client $client = null) { - $this->accessToken = $token; + $this->cacheManager = $cacheManager; + $this->client = $client ?: new Client(); } /** - * @return Hydrator\Feed + * @return Hydrator\Component\Feed * @throws InstagramException - * @throws \GuzzleHttp\Exception\GuzzleException */ public function getFeed() { - if (!$this->userId) { - throw new InstagramException('Missing userId'); + if (empty($this->userName)) { + throw new InstagramException('Username cannot be empty'); } - if (!$this->accessToken) { - throw new InstagramException('Missing access token'); + if ($this->endCursor) { + $feed = new JsonTransportFeed($this->cacheManager, $this->client, $this->endCursor); + $hydrator = new JsonHydrator(); + } else { + $feed = new HtmlTransportFeed($this->cacheManager, $this->client); + $hydrator = new HtmlHydrator(); } - $feed = new JsonFeed($this->clientUser, $this->clientMedia, $this->accessToken); - $hydrator = new Hydrator(); - - $userDataFetched = $feed->fetchUserData($this->userId); - $hydrator->setUserData($userDataFetched); + $dataFetched = $feed->fetchData($this->userName); - $mediaDataFetched = $feed->fetchMediaData($this->userId, $this->maxId); - $hydrator->setMediaData($mediaDataFetched); + $hydrator->setData($dataFetched); return $hydrator->getHydratedData(); } /** - * @param string $maxId + * @param string $userName + */ + public function setUserName($userName) + { + $this->userName = $userName; + } + + /** + * @param string $endCursor */ - public function setMaxId($maxId) + public function setEndCursor($endCursor) { - $this->maxId = $maxId; + $this->endCursor = $endCursor; } } diff --git a/src/Instagram/Exception/CacheException.php b/src/Instagram/Exception/CacheException.php new file mode 100644 index 0000000..78d2c6c --- /dev/null +++ b/src/Instagram/Exception/CacheException.php @@ -0,0 +1,7 @@ +userData = $userData; - } - - /** - * @param array $mediaData - */ - public function setMediaData($mediaData) - { - $this->mediaData = $mediaData; - } - - /** - * @return Feed - */ - public function getHydratedData() - { - $feed = $this->generateFeed(); - - if (isset($this->mediaData['data'][0])) { - foreach ($this->mediaData['data'] as $node) { - $media = new Media(); - - $media->setId($node['id']); - $media->setTypeName($node['type']); - - $media->setCaption($node['caption']['text']); - - $media->setHeight($node['images']['standard_resolution']['height']); - $media->setWidth($node['images']['standard_resolution']['width']); - - $media->setThumbnailSrc($node['images']['thumbnail']['url']); - $media->setDisplaySrc($node['images']['standard_resolution']['url']); - - $media->setLink($node['link']); - - $date = new \DateTime(); - $date->setTimestamp($node['created_time']); - - $media->setDate($date); - - $media->setComments($node['comments']['count']); - $media->setLikes($node['likes']['count']); - - $feed->addMedia($media); - } - - $feed->setHasNextPage(isset($this->mediaData['pagination']['next_max_id'])); - $feed->setMaxId(isset($this->mediaData['pagination']['next_max_id']) ? $this->mediaData['pagination']['next_max_id'] : null); - } - - return $feed; - } - - /** - * @return Feed - */ - private function generateFeed() - { - $feed = new Feed(); - - if ($this->userData) { - $feed->setId($this->userData['id']); - $feed->setUserName($this->userData['username']); - $feed->setBiography($this->userData['bio']); - $feed->setFullName($this->userData['full_name']); - $feed->setProfilePicture($this->userData['profile_picture']); - $feed->setMediaCount($this->userData['counts']['media']); - $feed->setFollowers($this->userData['counts']['followed_by']); - $feed->setFollowing($this->userData['counts']['follows']); - $feed->setExternalUrl($this->userData['website']); - } - - return $feed; - } -} diff --git a/src/Instagram/Hydrator/Feed.php b/src/Instagram/Hydrator/Component/Feed.php similarity index 81% rename from src/Instagram/Hydrator/Feed.php rename to src/Instagram/Hydrator/Component/Feed.php index 9d70130..08ca3b6 100644 --- a/src/Instagram/Hydrator/Feed.php +++ b/src/Instagram/Hydrator/Component/Feed.php @@ -1,5 +1,6 @@ hasNextPage; - } - - /** - * @param bool $hasNextPage - */ - public function setHasNextPage($hasNextPage) - { - $this->hasNextPage = $hasNextPage; - } - - /** - * @return array + * @return Media[] */ public function getMedias() { @@ -232,9 +212,9 @@ public function getMedias() } /** - * @param $media + * @param Media $media */ - public function addMedia($media) + public function addMedia(Media $media) { $this->medias[] = $media; } @@ -242,16 +222,16 @@ public function addMedia($media) /** * @return string */ - public function getMaxId() + public function getEndCursor() { - return $this->maxId; + return $this->endCursor; } /** - * @param string $maxId + * @param string $endCursor */ - public function setMaxId($maxId) + public function setEndCursor($endCursor) { - $this->maxId = $maxId; + $this->endCursor = $endCursor; } } diff --git a/src/Instagram/Hydrator/Media.php b/src/Instagram/Hydrator/Component/Media.php similarity index 98% rename from src/Instagram/Hydrator/Media.php rename to src/Instagram/Hydrator/Component/Media.php index 14ddf38..94f6ce3 100644 --- a/src/Instagram/Hydrator/Media.php +++ b/src/Instagram/Hydrator/Component/Media.php @@ -1,5 +1,6 @@ data = $data; + } + + /** + * @return Feed + */ + public function getHydratedData() + { + $feed = $this->generateFeed(); + + foreach ($this->data->edge_owner_to_timeline_media->edges as $edge) { + + /** @var \stdClass $node */ + $node = $edge->node; + + $media = new Media(); + + $media->setId($node->id); + $media->setTypeName($node->__typename); + + if ($node->edge_media_to_caption->edges) { + $media->setCaption($node->edge_media_to_caption->edges[0]->node->text); + } + + $media->setHeight($node->dimensions->height); + $media->setWidth($node->dimensions->width); + + $media->setThumbnailSrc($node->thumbnail_src); + $media->setDisplaySrc($node->display_url); + + $date = new \DateTime(); + $date->setTimestamp($node->taken_at_timestamp); + + $media->setDate($date); + + $media->setComments($node->edge_media_to_comment->count); + $media->setLikes($node->edge_liked_by->count); + + $media->setLink(TransportFeed::INSTAGRAM_ENDPOINT . "p/{$node->shortcode}/"); + + $feed->addMedia($media); + } + + return $feed; + } + + /** + * @return Feed + */ + private function generateFeed() + { + $feed = new Feed(); + + $feed->setId($this->data->id); + $feed->setUserName($this->data->username); + $feed->setBiography($this->data->biography); + $feed->setFullName($this->data->full_name); + $feed->setProfilePicture($this->data->profile_pic_url_hd); + $feed->setMediaCount($this->data->edge_owner_to_timeline_media->count); + $feed->setFollowers($this->data->edge_followed_by->count); + $feed->setFollowing($this->data->edge_follow->count); + $feed->setExternalUrl($this->data->external_url); + $feed->setEndCursor($this->data->edge_owner_to_timeline_media->page_info->end_cursor); + + return $feed; + } +} diff --git a/src/Instagram/Hydrator/JsonHydrator.php b/src/Instagram/Hydrator/JsonHydrator.php new file mode 100644 index 0000000..efe5989 --- /dev/null +++ b/src/Instagram/Hydrator/JsonHydrator.php @@ -0,0 +1,77 @@ +data = $data; + } + + /** + * @return Feed + */ + public function getHydratedData() + { + $feed = $this->generateFeed(); + + foreach ($this->data->edge_owner_to_timeline_media->edges as $edge) { + + /** @var \stdClass $node */ + $node = $edge->node; + + $media = new Media(); + + $media->setId($node->id); + $media->setTypeName($node->__typename); + + if ($node->edge_media_to_caption->edges) { + $media->setCaption($node->edge_media_to_caption->edges[0]->node->text); + } + + $media->setHeight($node->dimensions->height); + $media->setWidth($node->dimensions->width); + + $media->setThumbnailSrc($node->thumbnail_src); + $media->setDisplaySrc($node->display_url); + + $date = new \DateTime(); + $date->setTimestamp($node->taken_at_timestamp); + + $media->setDate($date); + + $media->setComments($node->edge_media_to_comment->count); + $media->setLikes($node->edge_media_preview_like->count); + + $media->setLink(TransportFeed::INSTAGRAM_ENDPOINT . "p/{$node->shortcode}/"); + + $feed->addMedia($media); + } + + return $feed; + } + + /** + * @return Feed + */ + private function generateFeed() + { + $feed = new Feed(); + + $feed->setEndCursor($this->data->edge_owner_to_timeline_media->page_info->end_cursor); + return $feed; + } +} diff --git a/src/Instagram/Storage/Cache.php b/src/Instagram/Storage/Cache.php new file mode 100644 index 0000000..421918e --- /dev/null +++ b/src/Instagram/Storage/Cache.php @@ -0,0 +1,69 @@ +rhxGis; + } + + /** + * @param string $rhxGis + */ + public function setRhxGis($rhxGis) + { + $this->rhxGis = $rhxGis; + } + + /** + * @return array + */ + public function getCookie() + { + return $this->cookie; + } + + /** + * @param array $cookie + */ + public function setCookie($cookie) + { + $this->cookie = $cookie; + } + + /** + * @return int + */ + public function getUserId() + { + return $this->userId; + } + + /** + * @param int $userId + */ + public function setUserId($userId) + { + $this->userId = $userId; + } +} diff --git a/src/Instagram/Storage/CacheManager.php b/src/Instagram/Storage/CacheManager.php new file mode 100644 index 0000000..9964481 --- /dev/null +++ b/src/Instagram/Storage/CacheManager.php @@ -0,0 +1,70 @@ +cacheDir = $cacheDir ?: $this->cacheDir; + } + + /** + * @param $userId + * @return string + */ + private function getCacheFile($userId) + { + return ($this->cacheDir ? $this->cacheDir : __DIR__ . '/../../../cache/') . $userId . '.cache'; + } + + /** + * @param $userId + * @return Cache|mixed + */ + public function getCache($userId) + { + if (is_file($this->getCacheFile($userId))) { + $handle = fopen($this->getCacheFile($userId), 'r'); + $data = fread($handle, filesize($this->getCacheFile($userId))); + $cache = unserialize($data); + + fclose($handle); + + if ($cache instanceof Cache) { + return $cache; + } + } + + return new Cache(); + } + + /** + * @param Cache $cache + * @param $userName + * @throws CacheException + */ + public function set(Cache $cache, $userName) + { + if (!is_writable(dirname($this->getCacheFile($userName)))) { + throw new CacheException('Cache folder is not writable'); + } + + $data = serialize($cache); + $handle = fopen($this->getCacheFile($userName), 'w+'); + + fwrite($handle, $data); + fclose($handle); + } +} diff --git a/src/Instagram/Transport/HtmlTransportFeed.php b/src/Instagram/Transport/HtmlTransportFeed.php new file mode 100644 index 0000000..cb88cbf --- /dev/null +++ b/src/Instagram/Transport/HtmlTransportFeed.php @@ -0,0 +1,64 @@ + [ + 'user-agent' => self::USER_AGENT + ] + ]; + + $res = $this->client->request('GET', $endpoint, $headers); + + $html = (string)$res->getBody(); + + preg_match('/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + +