Adding Instagram photos to my site with AngularJS

I tried to add my latest Instagram photos to my personal site using my own look and feel despite Instagram not wanting me to do this. It wasn’t pretty but I just about got it done. This is how.

Why?

My personal website (http://andybarefoot.com/) is like many others. It exists because I work in digital and therefore my name must also be a URL. I expect it to get approximately zero visitors and I am not disappointed. However if someone was to visit it would be nice to have some content. I have very little content.

Its other purpose is to be an occasional test bed for any web dev stuff that I think I should know about. Therefore it is responsive. It uses SASS. The code is on GitHub. And now I have decided to learn some AngularJS. So lets find some Angular functionality to shoehorn in.

Adding a social feed to the site will bump up the content and allow me to implement a binding between the view and an external API. Very Angular.

I’m going to start with Instagram because I occasionally post to Instagram and the content is big and colourful. Ideal for filling up a dry page. What’s more, Günter (my dog) is a regular Instagrammer and nothing says digital professional more than dog photos all over your personal site.


What?

So, this is what I will do. The page will initially show the latest 6 photos from mine and Günter’s Instagram feeds. The user can then click a “Load More” button to see another 6 photos. They can do this until our entire photographic archive is displayed at which point a sad “No More” message will be displayed and they can click no longer. I expect this to be the least displayed message in the history of the internet.

Simple social grid with a Masonry layout displaying photos and videos from Instagram.

Instagram API integration (or not)

I’ve used the Instagram API in the past so getting the latest content should be a doddle. But then I read this: INSTAGRAM TO THIRD-PARTY DEVELOPERS: DROP DEAD

Instagram have made some changes to the API since I last looked. My “app” is in the sandbox and as such has limited functionality, including limited access to only the last 20 photos from my feed. To get out of the Sandbox my app needs to meet one of only 3 use cases that Instagram approve:

  • My app allows people to login with Instagram and share their own content.
  • My product helps brands and advertisers understand, manage their audience and media rights.
  • My product helps broadcasters and publishers discover content, get digital rights to media, and share media with proper attribution.

No. No. And No.

Instead I will need to bodge this. I could still use the API to get the latest 20 photos, store this information and then run a regular script to update the stored information when new photos are published. However this will only give me all photos going forward, the older photos in my feed will not be displayed. And this would mean that bathroom selfie I took in November 2012 would never be displayed. Obviously this is unacceptable.


Scraping Instagram

So my new plan is to scrape the content from the Instagram site. The page https://www.instagram.com/andybarefoot/ shows the latest 12 photos from my feed and I can click “load more” repeatedly to eventually see all my photos. The data for the initial 12 photos is in the source of the loaded page and looks something like this:

<script type="text/javascript">
window._sharedData = {"country_code": "GB", "language_code": "en", "gatekeepers": {"vvc": true, "sulgin": true, "sfbf": true}, "show_app_install": false, "static_root": "//instagramstatic-a.akamaihd.net/h1", "platform": "web", "hostname": "www.instagram.com", "entry_data": {"ProfilePage": [{"user": {"username": "andybarefoot", "follows": {"count": 74}, "requested_by_viewer": false, "followed_by": {"count": 182}, "country_block": null, "has_requested_viewer": false, "external_url_linkshimmed": "http://www.andybarefoot.com", "follows_viewer": false, "profile_pic_url": "https://scontent-lhr3-1.cdninstagram.com/t51.2885-19/11821957_1160852530608038_111931099_a.jpg", "biography": null, "full_name": "Andy Barefoot", "media": {"count": 145, "page_info": {"has_previous_page": false, "start_cursor": "1206555795924753548", "end_cursor": "898341417301614125", "has_next_page": true}, "nodes": [{"code": "BC-jBqRyJyM", "date": 1458052680, "dimensions": {"width": 640, "height": 640}, "comments": {"count": 3}, "caption": "Let's make some games! First mobile puzzle game launched today: guntergames.com/colorhex", "likes": {"count": 14}, "owner": {"id": "1539646"}, "thumbnail_src": "https://scontent-lhr3-1.cdninstagram.com/t51.2885-15/s640x640/sh0.08/e35/12797676_525101191004225_1230748974_n.jpg?ig_cache_key=MTIwNjU1NTc5NTkyNDc1MzU0OA%3D%3D.2", "is_video": false, "id": "1206555795924753548", "display_src": "https://scontent-lhr3-1.cdninstagram.com/t51.2885-15/s640x640/sh0.08/e35/12797676_525101191004225_1230748974_n.jpg?ig_cache_key=MTIwNjU1NTc5NTkyNDc1MzU0OA%3D%3D.2"}, {"code": "BAow79CSJ16", "date": 1453026809, "dimensions": {"width": 1080, "height": 1080}, "comments": {"count": 0}, "caption": "Husky training Day 1 #dogs #dogsofinstagram #mickleovertundra", "likes": {"count": 11}, "owner": {"id": "1539646"}, "thumbnail_src": "https://scontent-lhr3-1.cdninstagram.com/t51.2885-15/s640x640/sh0.08/e35/12568743_941603902555109_846971495_n.jpg?ig_cache_key=MTE2NDM5NTcyOTg5MzQ5ODIzNA%3D%3D.2", "is_video": false, "id": "1164395729893498234", "display_src": "https://scontent-lhr3-1.cdninstagram.com/t51.2885-15/e35/12568743_941603902555109_846971495_n.jpg?ig_cache_key=MTE2NDM5NTcyOTg5MzQ5ODIzNA%3D%3D.2"}, {"code": "_2FtQnSJ7T", "date": 1451326423, "dimensions": {"width": 1080, "height": 1080}, "comments": {"count": 0}, "caption": "5 minutes until the start of The Force Awakens. Derby holds it breath.", "likes": {"count": 8}, "owner": {"id": "1539646"}, "thumbnail_src": "https://scontent-lhr3-1.cdninstagram.com/t51.2885-15/s640x640/sh0.08/e35/11349298_1024548830922031_1398138892_n.jpg?ig_cache_key=MTE1MDEzMTg1NTI4NzY4ODkxNQ%3D%3D.2", "is_video": false, "id": "1150131855287688915", "display_src": "https://scontent-lhr3-1.cdninstagram.com/t51.2885-15/e35/11349298_1024548830922031_1398138892_n.jpg?ig_cache_key=MTE1MDEzMTg1NTI4NzY4ODkxNQ%3D%3D.2"},...
</script>

A nice big chunk of JSON defining each photo as a “Node”. Very promising.

And on load of each set of 12 new photos the browser receives content looking like this:

{“status”: “ok”, “media”: {“count”: 145, “page_info”: {“has_previous_page”: true, “start_cursor”: “795811422692482221”, “end_cursor”: “741296626073115965”, “has_next_page”: true}, “nodes”: [{“code”: “sLSkXFyJyt”, “date”: 1409088136, “dimensions”: {“width”: 640, “height”: 640}, “comments”: {“count”: 0}, “caption”: “New hyperlapse app makes professional quality time lapse video. As demonstrated here by Joellen Bigelow…”, “likes”: {“count”: 1}, “owner”: {“id”: “1539646”}, “thumbnail_src”: “https://scontent-lhr3-1.cdninstagram.com/t51.2885-15/e15/10601720_1531422703756362_1045553821_n.jpg?ig_cache_key=Nzk1ODExNDIyNjkyNDgyMjIx.2", “is_video”: true, “id”: “795811422692482221”, “display_src”: “https://scontent-lhr3-1.cdninstagram.com/t51.2885-15/e15/10601720_1531422703756362_1045553821_n.jpg?ig_cache_key=Nzk1ODExNDIyNjkyNDgyMjIx.2"}, {“code”: “sClzZ7yJ17”, “date”: 1408796230, “dimensions”: {“width”: 640, “height”: 640}, “comments”: {“count”: 0}, “caption”: “Fellow Deutschlanders, is it too soon to be switching to winter tires? #letsoffroad”, “likes”: {“count”: 2}, “owner”: {“id”: “1539646”}, “thumbnail_src”: “https://scontent-lhr3-1.cdninstagram.com/t51.2885-15/e15/10597358_1518059295074807_120835669_n.jpg?ig_cache_key=NzkzMzYyNzQ0NjMxNDAxODUx.2", “is_video”: false, “id”: “793362744631401851”, “display_src”: “https://scontent-lhr3-1.cdninstagram.com/t51.2885-15/e15/10597358_1518059295074807_120835669_n.jpg?ig_cache_key=NzkzMzYyNzQ0NjMxNDAxODUx.2"},... ]}}

A very similar chunk of JSON also defining the new photos as “Nodes” in the same way. To get started I manually grab all this content and paste it into a new text file so I have JSON format data for all my photos: www.andybarefoot.com/api/instagram/andybarefoot.txt

I do the same thing for Günter’s feed and now I have 2 text files containing the complete data defining the photos.

I’ve kept the content from the 2 Instagram feeds separate so that if I change my mind about having my slobbering hound all over my professional portfolio I can easily separate his content in the future.

To keep these files updated I write a PHP script to run every day that goes to https://www.instagram.com/andybarefoot/ and https://www.instagram.com/gunterguntychops and grabs the JSON shown above and adds any new photos to my text file. To do this it converts the JSON from the page and from the text file into 2 arrays of objects using “json_decode” and then loops through both arrays to see whether any photos in the page are not yet in the text file. It creates a new array with all photos in and then converts this back to a JSON format string and writes it to the text file.

The structured data is now auto-updating but I don’t have the actual photos or videos so I add the functionality to create and save a copy of these files to my daily script. For images this is easy, as the image file is given in the structured data. For video it is more complicated as the video is not referenced in the data. Instead looking at the Instagram site it seems that the data defining the video is delivered from another URL of the format: https://www.instagram.com/p/[post ID]/?taken-by=[user name]&__a=1

This URL isn’t defined in the data either so I manual create this URL, grab the JSON and parse it for the video URL which I then use to grab the video and save to my server. For both the video and the photo I add a new node to the data describing the local URL of the new photo/video.

This is the ugliest part of this project. The Instagram API provides much better support for getting the video URL and there is no guarantee that my solution will continue to work. If Instagram change the way they have built their site my script will break and I will need to rebuild this. However I have vowed not to use the Instagram API and a man got to have a code about code.

So my decision not to use the Instagram API and instead grab the data, photos and videos from the Instagram site has meant a bit more work and has meant my solution is vulnerable if Instagram decide to change their site. However it does mean I don’t have to use the Instagram API and I do have all my Instagram content saved on my own server in case Instagram lose my content or delete my account.


Content API

Now I have all my content I want a simple API so that my page can access this content. I write another PHP script that opens the 2 text files, combines the content, orders the objects by date order (latest first) and returns photos based on the count and offset requested. It looks like this:

<?
$COUNT = intval($_GET[“count”]);
if($COUNT<=0)$COUNT=5;
$OFFSET = intval($_GET[“offset”]);
if($OFFSET<=0)$OFFSET=0;
function cmp($a, $b){
return strcmp(intval($b[‘date’]), intval($a[‘date’]));
}
$andyText = file_get_contents(‘andybarefoot.txt’);
$gunterText = file_get_contents(‘gunterguntychops.txt’);
$andyData = json_decode($andyText, true);
$gunterData = json_decode($gunterText, true);
$andyNodes = $andyData[‘nodes’];
$gunterNodes = $gunterData[‘nodes’];
$allNodes = array_merge($andyNodes,$gunterNodes);
usort($allNodes, “cmp”);
$nodes = array_slice($allNodes, $OFFSET, $COUNT);
$data = [
“offset” => $OFFSET,
“count” => $COUNT,
];
$data[“nodes”] = $nodes;
print json_encode($data);
?>

I can now request this script to get the photos I want in JSON format, e.g: http://www.andybarefoot.com/api/instagram/allData.php?offset=0&count=6


Loading content with AngularJS

To pull this content into my page I use the Angular $http service. Once added to my controller it becomes trivial. I have a “loadMoreSocial” function which I can all from my “Load More” button and also call on load to pre-fill the first set of photos.

I set “offset” and “count” variables to control the photos requested. The function retrieves the data based on these variables and pushes the data to an “instagrams” array. It then increases the offset so that the next time it is called it will retrieve an older set of photos.

andybarefoot.controller(‘mainController’, [‘$scope’, ‘$filter’, ‘$http’, function ($scope, $filter, $http) {
$scope.instagramOffset = 0;
$scope.instagramCount = 6;
$scope.instagrams = [];
$scope.loadMoreSocial = function () {
url = ‘/api/instagram/allData.php?offset=' + $scope.instagramOffset + ‘&count=’ + $scope.instagramCount;
$http.get(url)
.success(function (result){
if(result.nodes.length>0){
$scope.instagrams.push.apply($scope.instagrams, result.nodes);
$scope.instagramOffset += $scope.instagramCount;
}
})
.error(function (data,status){
console.log(data);
})
};
$scope.loadMoreSocial();
}]);

Now I have everything I need to add the content to the page.


Displaying content

My responsive design allows the social feed to occupy 1, 2 or 3 columns so I use the Masonry plug-in to rearrange the photos nicely once they have loaded.

Masonry layout: http://masonry.desandro.com/
Directive to use AngularJS: http://passy.github.io/angular-masonry/

Now I just map the data in the “instagrams” array to the page. For every object in the Instagram array Angular will generate a “item” div.

<div masonry  preserve-order id="container">
<div class="item masonry-brick" ng-repeat="instagram in instagrams"></div><!-- class: item -->
</div><!-- container -->

Within each div I display the text with the account name and the date.

<div class="dateText">
<a ng-href="{{userURLString(instagram.owner.id)}}" target="_blank">
<img src="images/icons/png32/instagram.png" width="24" height="24" />
</a>
{{ userNameString(instagram.owner.id) }}
{{ instagram.date * 1000 | date:'d MMMM yyyy' }}
</div><!-- class: dateText -->

I write simple functions to return a string based on the Instagram account owner ID and the URL of the relevant Instagram page. I convert the Instagram time stamp from seconds to a formatted date.

I then display the image. If the post is a video post I also display an overlay image with a play button that the user clicks to load the video.

<img ng-click="instagram.is_clicked = true" class="video-play" ng-if="instagram.is_video && !instagram.is_clicked" src="images/video-icon.png" />
<img ng-src="{{ getImage(instagram.local_image) }}" alt="{{ instagram.caption }}" ng-show="!instagram.is_clicked" />
<video ng-if="instagram.is_video && instagram.is_clicked" controls="controls" autoplay poster="{{ getImage(instagram.local_image) }}" name="Video Name" ng-src="{{ getVideo(instagram.local_video) }}"></video>

I display the overlay based on the instagram.is_video value. Using ng-if means that the element is only added to the DOM if this condition is true. I add an ng-click function to the overlay that sets a value “is_clicked” for this instance of “instagram”.

I then use this “is_clicked” value to determine whether the image should still be shown or whether it should be hidden and the video player loaded in its place. I set the video player to “autoplay” and add the image as “poster” to make the loading and playing of the video as smooth as possible.

Below the image I simply add the caption and the display is complete.

<p>{{ instagram.caption }}</p>

Finally below the photos I add 2 elements to display either the “Load more” or ‘No more” messages.

<div id=”more” class=”dateText” ng-show=”showMoreSocial” ng-click=”loadMoreSocial()”>
LOAD MORE SOCIAL FEED
</div>
<div id=”all” class=”dateText” ng-show=”noMoreSocial”>
SOCIAL FEED FULLY LOADED
</div>

These will be shown when the 2 variables “showMoreSocial” and “noMoreSocial” are true. I simply need to add some logic to the “loadMoreSocial” function to show the relevant message based on the number of results I get back.

if(result.nodes.length>0){
$scope.instagrams.push.apply($scope.instagrams, result.nodes);
$scope.instagramOffset += $scope.instagramCount;
$scope.showMoreSocial = true;
$scope.noMoreSocial = false;
}else{
$scope.showMoreSocial = false;
$scope.noMoreSocial = true;
}

Finished!

That’s just about it. The finished result can be seen here: http://andybarefoot.com/

If you want to reuse any of this code it is here: https://github.com/andybarefoot/andybarefoot-www

If you want to tell me how I have done this in a horrible way and should have done it differently please feel free.