├── python_instagram.egg-info ├── zip-safe ├── dependency_links.txt ├── top_level.txt ├── requires.txt ├── PKG-INFO └── SOURCES.txt ├── .gitignore ├── fixtures ├── like_media.json ├── unlike_media.json ├── delete_comment.json ├── follow_user.json ├── tag.json ├── change_user_relationship.json ├── user_no_auth_user.json ├── location.json ├── user_followed_by.json ├── user.json ├── user_incoming_requests.json ├── user_search.json ├── create_media_comment.json ├── media_likes.json ├── location_search.json ├── tag_search.json ├── user_media_feed.json ├── user_liked_media.json ├── media_shortcode.json ├── media.json ├── user_recent_media.json ├── tag_recent_media.json ├── user_follows.json ├── media_search.json └── geography_recent_media.json ├── instagram ├── __init__.py ├── helper.py ├── json_import.py ├── subscriptions.py ├── models.py ├── bind.py ├── client.py └── oauth2.py ├── dist ├── python-instagram-1.0.0.tar.gz ├── python-instagram-1.0.1.tar.gz ├── python-instagram-1.1.0.tar.gz ├── python-instagram-1.1.1.tar.gz └── python-instagram-1.1.2.tar.gz ├── requirements.txt ├── .travis.yml ├── setup.py ├── get_access_token.py ├── LICENSE.md ├── tests.py ├── README.md └── sample_app.py /python_instagram.egg-info/zip-safe: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /python_instagram.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /python_instagram.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | instagram 2 | -------------------------------------------------------------------------------- /python_instagram.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | simplejson 2 | httplib2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | *.swp 4 | test_settings.py 5 | 6 | -------------------------------------------------------------------------------- /fixtures/like_media.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": null 6 | } -------------------------------------------------------------------------------- /fixtures/unlike_media.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": null 6 | } -------------------------------------------------------------------------------- /fixtures/delete_comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": null 6 | } -------------------------------------------------------------------------------- /instagram/__init__.py: -------------------------------------------------------------------------------- 1 | from .bind import InstagramAPIError, InstagramClientError 2 | from .client import InstagramAPI 3 | -------------------------------------------------------------------------------- /dist/python-instagram-1.0.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/python-instagram/HEAD/dist/python-instagram-1.0.0.tar.gz -------------------------------------------------------------------------------- /dist/python-instagram-1.0.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/python-instagram/HEAD/dist/python-instagram-1.0.1.tar.gz -------------------------------------------------------------------------------- /dist/python-instagram-1.1.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/python-instagram/HEAD/dist/python-instagram-1.1.0.tar.gz -------------------------------------------------------------------------------- /dist/python-instagram-1.1.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/python-instagram/HEAD/dist/python-instagram-1.1.1.tar.gz -------------------------------------------------------------------------------- /dist/python-instagram-1.1.2.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facebookarchive/python-instagram/HEAD/dist/python-instagram-1.1.2.tar.gz -------------------------------------------------------------------------------- /fixtures/follow_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": { 6 | "outgoing_status": "follows" 7 | } 8 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bottle==0.12.7 2 | httplib2==0.9 3 | python-instagram==1.1.3 4 | redis==2.10.3 5 | simplejson==3.6.3 6 | beaker==1.6.4 7 | six==1.8.0 8 | -------------------------------------------------------------------------------- /fixtures/tag.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": { 6 | "media_count": 3, 7 | "name": "iphone" 8 | } 9 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.3" 6 | - "3.4" 7 | install: 8 | - "pip install ." 9 | script: "python tests.py" 10 | 11 | -------------------------------------------------------------------------------- /fixtures/change_user_relationship.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": { 6 | "outgoing_status": "follows", 7 | "incoming_status": "requested_by" 8 | } 9 | } -------------------------------------------------------------------------------- /fixtures/user_no_auth_user.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "error_type": "no authenticated user", 4 | "code": 400, 5 | "error_message": "'self' keyword requires an authenticated user" 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /instagram/helper.py: -------------------------------------------------------------------------------- 1 | import calendar 2 | from datetime import datetime 3 | 4 | 5 | def timestamp_to_datetime(ts): 6 | return datetime.utcfromtimestamp(float(ts)) 7 | 8 | 9 | def datetime_to_timestamp(dt): 10 | return calendar.timegm(dt.timetuple()) 11 | -------------------------------------------------------------------------------- /fixtures/location.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": { 6 | "latitude": 37.769298430744279, 7 | "id": "315", 8 | "longitude": -122.4290335178376, 9 | "name": "Duboce & Church MUNI Stop" 10 | } 11 | } -------------------------------------------------------------------------------- /fixtures/user_followed_by.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": [ 6 | { 7 | "username": "shayne", 8 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_20_75sq_1295492590_debug.jpg", 9 | "id": "20" 10 | }] 11 | } -------------------------------------------------------------------------------- /fixtures/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": { 6 | "username": "mikeyk", 7 | "first_name": "Mike", 8 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_4_75sq_1292324747_debug.jpg", 9 | "id": "4", 10 | "last_name": "Krieger!!" 11 | } 12 | } -------------------------------------------------------------------------------- /instagram/json_import.py: -------------------------------------------------------------------------------- 1 | try: 2 | import simplejson 3 | except ImportError: 4 | try: 5 | import json as simplejson 6 | except ImportError: 7 | try: 8 | from django.utils import simplejson 9 | except ImportError: 10 | raise ImportError('A json library is required to use this python library') 11 | -------------------------------------------------------------------------------- /python_instagram.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.0 2 | Name: python-instagram 3 | Version: 1.1.3 4 | Summary: Instagram API client 5 | Home-page: http://github.com/Instagram/python-instagram 6 | Author: Instagram, Inc 7 | Author-email: apidevelopers@instagram.com 8 | License: MIT 9 | Description: UNKNOWN 10 | Keywords: instagram 11 | Platform: UNKNOWN 12 | -------------------------------------------------------------------------------- /fixtures/user_incoming_requests.json: -------------------------------------------------------------------------------- 1 | { 2 | "pagination": { 3 | }, 4 | "meta": { 5 | "code": 200 6 | }, 7 | "data": [ 8 | { 9 | "username": "kevin", 10 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_3_75sq_1295493319_debug.jpg", 11 | "id": "3" 12 | } 13 | ] 14 | } 15 | 16 | -------------------------------------------------------------------------------- /fixtures/user_search.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": [ 6 | { 7 | "username": "iphone", 8 | "first_name": "Insta", 9 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 10 | "id": "148117", 11 | "last_name": "Gram" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /fixtures/create_media_comment.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": { 6 | "created_time": "1296770323", 7 | "text": "o_hai", 8 | "from": { 9 | "username": "mikeyk", 10 | "first_name": "Mike", 11 | "last_name": "Krieger!!", 12 | "type": "user", 13 | "id": "4" 14 | }, 15 | "id": "2611747" 16 | } 17 | } -------------------------------------------------------------------------------- /python_instagram.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | setup.py 2 | instagram/__init__.py 3 | instagram/bind.py 4 | instagram/client.py 5 | instagram/helper.py 6 | instagram/json_import.py 7 | instagram/models.py 8 | instagram/oauth2.py 9 | instagram/subscriptions.py 10 | python_instagram.egg-info/PKG-INFO 11 | python_instagram.egg-info/SOURCES.txt 12 | python_instagram.egg-info/dependency_links.txt 13 | python_instagram.egg-info/requires.txt 14 | python_instagram.egg-info/top_level.txt 15 | python_instagram.egg-info/zip-safe -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | 4 | setup(name="python-instagram", 5 | version="1.3.2", 6 | description="Instagram API client", 7 | license="MIT", 8 | install_requires=["simplejson","httplib2","six"], 9 | author="Instagram, Inc", 10 | author_email="apidevelopers@instagram.com", 11 | url="http://github.com/Instagram/python-instagram", 12 | packages = find_packages(), 13 | keywords= "instagram", 14 | zip_safe = True) 15 | -------------------------------------------------------------------------------- /fixtures/media_likes.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": [ 6 | { 7 | "username": "jec", 8 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_16_75sq_1288978412.jpg", 9 | "id": "16" 10 | }, 11 | { 12 | "username": "mikeyk", 13 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_4_75sq_1292324747_debug.jpg", 14 | "id": "4" 15 | }, 16 | { 17 | "username": "nicole", 18 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_6_75sq_1285365377.jpg", 19 | "id": "6" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /fixtures/location_search.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": [ 6 | { 7 | "latitude": 37.775382999999991, 8 | "id": "74480", 9 | "longitude": -122.223941, 10 | "name": "Fruitvale BART" 11 | }, 12 | { 13 | "latitude": 37.775289999999998, 14 | "id": "163042", 15 | "longitude": -122.224172, 16 | "name": "Powderface" 17 | }, 18 | { 19 | "latitude": 37.775180799999987, 20 | "id": "68841", 21 | "longitude": -122.2270716, 22 | "name": "El Novillo Taco Truck" 23 | }, 24 | { 25 | "latitude": 37.775194499999998, 26 | "id": "101132", 27 | "longitude": -122.227087, 28 | "name": "Guadalajara Mexican Cuisine and Bar" 29 | } 30 | ] 31 | } -------------------------------------------------------------------------------- /fixtures/tag_search.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": [ 6 | { 7 | "media_count": 3, 8 | "name": "iphone" 9 | }, 10 | { 11 | "media_count": 79, 12 | "name": "iphonegraphy" 13 | }, 14 | { 15 | "media_count": 57, 16 | "name": "iphoneography" 17 | }, 18 | { 19 | "media_count": 4, 20 | "name": "iphonegrapy" 21 | }, 22 | { 23 | "media_count": 2, 24 | "name": "iphone4" 25 | }, 26 | { 27 | "media_count": 1, 28 | "name": "iphonejp" 29 | }, 30 | { 31 | "media_count": 1, 32 | "name": "iphonenograpgy" 33 | }, 34 | { 35 | "media_count": 1, 36 | "name": "iphoneapp" 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /get_access_token.py: -------------------------------------------------------------------------------- 1 | from instagram.client import InstagramAPI 2 | import sys 3 | 4 | if len(sys.argv) > 1 and sys.argv[1] == 'local': 5 | try: 6 | from test_settings import * 7 | 8 | InstagramAPI.host = test_host 9 | InstagramAPI.base_path = test_base_path 10 | InstagramAPI.access_token_field = "access_token" 11 | InstagramAPI.authorize_url = test_authorize_url 12 | InstagramAPI.access_token_url = test_access_token_url 13 | InstagramAPI.protocol = test_protocol 14 | except Exception: 15 | pass 16 | 17 | # Fix Python 2.x. 18 | try: 19 | import __builtin__ 20 | input = getattr(__builtin__, 'raw_input') 21 | except (ImportError, AttributeError): 22 | pass 23 | 24 | client_id = input("Client ID: ").strip() 25 | client_secret = input("Client Secret: ").strip() 26 | redirect_uri = input("Redirect URI: ").strip() 27 | raw_scope = input("Requested scope (separated by spaces, blank for just basic read): ").strip() 28 | scope = raw_scope.split(' ') 29 | # For basic, API seems to need to be set explicitly 30 | if not scope or scope == [""]: 31 | scope = ["basic"] 32 | 33 | api = InstagramAPI(client_id=client_id, client_secret=client_secret, redirect_uri=redirect_uri) 34 | redirect_uri = api.get_authorize_login_url(scope = scope) 35 | 36 | print ("Visit this page and authorize access in your browser: "+ redirect_uri) 37 | 38 | code = (str(input("Paste in code in query string after redirect: ").strip())) 39 | 40 | access_token = api.exchange_code_for_access_token(code) 41 | print ("access token: " ) 42 | print (access_token) 43 | 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For python-instagram software 4 | 5 | Copyright (c) 2014, Facebook, Inc. All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name Facebook nor the names of its contributors may be used to 18 | endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /instagram/subscriptions.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import hashlib 3 | from .json_import import simplejson 4 | 5 | class SubscriptionType: 6 | TAG = 'tag' 7 | USER = 'user' 8 | GEOGRAPHY = 'geography' 9 | LOCATION = 'location' 10 | 11 | 12 | class SubscriptionError(Exception): 13 | pass 14 | 15 | 16 | class SubscriptionVerifyError(SubscriptionError): 17 | pass 18 | 19 | 20 | class SubscriptionsReactor(object): 21 | 22 | def __init__(self): 23 | self.callbacks = {} 24 | 25 | def _process_update(self, update): 26 | object_callbacks = self.callbacks.get(update['object'], []) 27 | 28 | for callback in object_callbacks: 29 | callback(update) 30 | 31 | def process(self, client_secret, raw_response, x_hub_signature): 32 | if not self._verify_signature(client_secret, raw_response, x_hub_signature): 33 | raise SubscriptionVerifyError("X-Hub-Signature and hmac digest did not match") 34 | 35 | try: 36 | response = simplejson.loads(raw_response) 37 | except ValueError: 38 | raise SubscriptionError('Unable to parse response, not valid JSON.') 39 | 40 | for update in response: 41 | self._process_update(update) 42 | 43 | def register_callback(self, object_type, callback): 44 | cb_list = self.callbacks.get(object_type, []) 45 | 46 | if callback not in cb_list: 47 | cb_list.append(callback) 48 | self.callbacks[object_type] = cb_list 49 | 50 | def deregister_callback(self, object_type, callback): 51 | callbacks = self.callbacks.get(object_type, []) 52 | callbacks.remove(callback) 53 | 54 | def _verify_signature(self, client_secret, raw_response, x_hub_signature): 55 | digest = hmac.new(client_secret.encode('utf-8'), 56 | msg=raw_response.encode('utf-8'), 57 | digestmod=hashlib.sha1 58 | ).hexdigest() 59 | return digest == x_hub_signature 60 | -------------------------------------------------------------------------------- /fixtures/user_media_feed.json: -------------------------------------------------------------------------------- 1 | { 2 | "pagination": { 3 | "next_url": "http://localhost:8000/publicapi/v1/users/self/feed?q=iphone&lat=37.771&access_token=DEBUG&lng=-122.221&max_id=3382520", 4 | "next_max_id": 3382520 5 | }, 6 | "meta": { 7 | "code": 200 8 | }, 9 | "data": [ 10 | { 11 | "type": "image", 12 | "location": { 13 | "latitude": 37.781089999999999, 14 | "id": null, 15 | "longitude": -122.3946, 16 | "name": null 17 | }, 18 | "tags": [], 19 | "comments": { 20 | "count": 1, 21 | "data": [ 22 | { 23 | "created_time": "1296272101", 24 | "text": "O hai.", 25 | "from": { 26 | "username": "mikeyk", 27 | "first_name": "Mike", 28 | "last_name": "Krieger!!", 29 | "type": "user", 30 | "id": "4" 31 | }, 32 | "id": "2611719" 33 | } 34 | ] 35 | }, 36 | "caption": { 37 | "created_time": "1296272101", 38 | "text": "O hai.", 39 | "from": { 40 | "username": "mikeyk", 41 | "first_name": "Mike", 42 | "last_name": "Krieger!!", 43 | "type": "user", 44 | "id": "4" 45 | }, 46 | "id": "2611719" 47 | }, 48 | "link": "http://localhost:8000/p/M5z4/", 49 | "likes": { 50 | "count": 0 51 | }, 52 | "created_time": "1296673135", 53 | "images": { 54 | "low_resolution": { 55 | "url": "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/b6073350d89644ccb253b89ead6c75da_6.jpg", 56 | "width": 480, 57 | "height": 480 58 | }, 59 | "thumbnail": { 60 | "url": "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/b6073350d89644ccb253b89ead6c75da_5.jpg", 61 | "width": 150, 62 | "height": 150 63 | }, 64 | "standard_resolution": { 65 | "url": "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/b6073350d89644ccb253b89ead6c75da_7.jpg", 66 | "width": 612, 67 | "height": 612 68 | } 69 | }, 70 | "user_has_liked": false, 71 | "id": "3382520", 72 | "user": { 73 | "username": "mikeyk", 74 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_4_75sq_1292324747_debug.jpg", 75 | "id": "4" 76 | } 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /fixtures/user_liked_media.json: -------------------------------------------------------------------------------- 1 | { 2 | "pagination": { 3 | "next_url": "http://localhost:8000/publicapi/v1/users/self/feed?q=iphone&lat=37.771&access_token=DEBUG&lng=-122.221&max_id=3382520", 4 | "next_max_like_id": 50 5 | }, 6 | "meta": { 7 | "code": 200 8 | }, 9 | "data": [ 10 | { 11 | "type": "image", 12 | "tags": [], 13 | "location": { 14 | "latitude": 37.781089999999999, 15 | "id": null, 16 | "longitude": -122.3946, 17 | "name": null 18 | }, 19 | "comments": { 20 | "count": 1, 21 | "data": [ 22 | { 23 | "created_time": "1296272101", 24 | "text": "O hai.", 25 | "from": { 26 | "username": "mikeyk", 27 | "first_name": "Mike", 28 | "last_name": "Krieger!!", 29 | "type": "user", 30 | "id": "4" 31 | }, 32 | "id": "2611719" 33 | } 34 | ] 35 | }, 36 | "caption": { 37 | "created_time": "1296272101", 38 | "text": "O hai.", 39 | "from": { 40 | "username": "mikeyk", 41 | "first_name": "Mike", 42 | "last_name": "Krieger!!", 43 | "type": "user", 44 | "id": "4" 45 | }, 46 | "id": "2611719" 47 | }, 48 | "link": "http://localhost:8000/p/M5z4/", 49 | "likes": { 50 | "count": 0 51 | }, 52 | "created_time": "1296673135", 53 | "images": { 54 | "low_resolution": { 55 | "url": "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/b6073350d89644ccb253b89ead6c75da_6.jpg", 56 | "width": 480, 57 | "height": 480 58 | }, 59 | "thumbnail": { 60 | "url": "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/b6073350d89644ccb253b89ead6c75da_5.jpg", 61 | "width": 150, 62 | "height": 150 63 | }, 64 | "standard_resolution": { 65 | "url": "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/b6073350d89644ccb253b89ead6c75da_7.jpg", 66 | "width": 612, 67 | "height": 612 68 | } 69 | }, 70 | "user_has_liked": false, 71 | "id": "3382520", 72 | "user": { 73 | "username": "mikeyk", 74 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_4_75sq_1292324747_debug.jpg", 75 | "id": "4" 76 | } 77 | } 78 | ] 79 | } 80 | 81 | -------------------------------------------------------------------------------- /fixtures/media_shortcode.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": { 6 | "attribution": null, 7 | "tags": [ 8 | "alegria" 9 | ], 10 | "type": "image", 11 | "location": null, 12 | "comments": { 13 | "count": 79623, 14 | "data": [] 15 | }, 16 | "filter": "Normal", 17 | "created_time": "1401623658", 18 | "link": "http://instagram.com/p/os1NQjxtvF/", 19 | "likes": { 20 | "count": 370660, 21 | "data": [ 22 | { 23 | "username": "wakamario0904", 24 | "profile_picture": "http://images.ak.instagram.com/profiles/profile_395645793_75sq_1400392412.jpg", 25 | "id": "395645793", 26 | "full_name": "$☆<-W@ka M@rio->☆$" 27 | }, 28 | { 29 | "username": "musaed_ms3", 30 | "profile_picture": "http://images.ak.instagram.com/profiles/profile_1353197338_75sq_1401757650.jpg", 31 | "id": "1353197338", 32 | "full_name": "Musaed" 33 | }, 34 | { 35 | "username": "kerllancritiina_", 36 | "profile_picture": "http://images.ak.instagram.com/profiles/profile_405413316_75sq_1401764697.jpg", 37 | "id": "405413316", 38 | "full_name": "Kerllan Santos" 39 | }, 40 | { 41 | "username": "marccelaaa", 42 | "profile_picture": "http://images.ak.instagram.com/profiles/profile_327540205_75sq_1401046535.jpg", 43 | "id": "327540205", 44 | "full_name": "Marcela Funes" 45 | } 46 | ] 47 | }, 48 | "images": { 49 | "low_resolution": { 50 | "url": "http://scontent-a.cdninstagram.com/hphotos-xpf1/t51.2885-15/10432037_1487631304805008_1552236767_a.jpg", 51 | "width": 306, 52 | "height": 306 53 | }, 54 | "thumbnail": { 55 | "url": "http://scontent-a.cdninstagram.com/hphotos-xpf1/t51.2885-15/10432037_1487631304805008_1552236767_s.jpg", 56 | "width": 150, 57 | "height": 150 58 | }, 59 | "standard_resolution": { 60 | "url": "http://scontent-a.cdninstagram.com/hphotos-xpf1/t51.2885-15/10432037_1487631304805008_1552236767_n.jpg", 61 | "width": 640, 62 | "height": 640 63 | } 64 | }, 65 | "users_in_photo": [], 66 | "caption": { 67 | "created_time": "1401623658", 68 | "text": "Bom diaaaaa !! #alegria #Alegria", 69 | "from": { 70 | "username": "neymarjr", 71 | "profile_picture": "http://images.ak.instagram.com/profiles/profile_26669533_75sq_1396546465.jpg", 72 | "id": "26669533", 73 | "full_name": "Nj" 74 | }, 75 | "id": "733194849033313209" 76 | }, 77 | "user_has_liked": false, 78 | "id": "733194846952938437_26669533", 79 | "user": { 80 | "username": "neymarjr", 81 | "website": "", 82 | "profile_picture": "http://images.ak.instagram.com/profiles/profile_26669533_75sq_1396546465.jpg", 83 | "full_name": "Nj", 84 | "bio": "", 85 | "id": "26669533" 86 | } 87 | } 88 | } 89 | 90 | -------------------------------------------------------------------------------- /fixtures/media.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": { 6 | "type": "image", 7 | "location": { 8 | "latitude": 37.782179999999997, 9 | "id": null, 10 | "longitude": -122.38849999999999, 11 | "name": null 12 | }, 13 | "comments": { 14 | "count": 6, 15 | "data": [ 16 | { 17 | "created_time": "1280379782", 18 | "text": "Next muni is in an hour; forget that, walking home instead", 19 | "from": { 20 | "username": "mikeyk", 21 | "first_name": "Mike", 22 | "last_name": "Krieger!!", 23 | "type": "user", 24 | "id": "4" 25 | }, 26 | "id": "367" 27 | }, 28 | { 29 | "created_time": "1280417247", 30 | "text": "I'm on the new Burbn!!! So beautiful, @kevin and @mikeyk!", 31 | "from": { 32 | "username": "grex", 33 | "first_name": "Gregor", 34 | "last_name": "Hochmuth", 35 | "type": "user", 36 | "id": "25" 37 | }, 38 | "id": "384" 39 | }, 40 | { 41 | "created_time": "1296711880", 42 | "text": "hawwoo", 43 | "from": { 44 | "username": "mikeyk", 45 | "first_name": "Mike", 46 | "last_name": "Krieger!!", 47 | "type": "user", 48 | "id": "4" 49 | }, 50 | "id": "2611722" 51 | }, 52 | { 53 | "created_time": "1296712932", 54 | "text": "foo", 55 | "from": { 56 | "username": "mikeyk", 57 | "first_name": "Mike", 58 | "last_name": "Krieger!!", 59 | "type": "user", 60 | "id": "4" 61 | }, 62 | "id": "2611725" 63 | }, 64 | { 65 | "created_time": "1296712932", 66 | "text": "foo", 67 | "from": { 68 | "username": "mikeyk", 69 | "first_name": "Mike", 70 | "last_name": "Krieger!!", 71 | "type": "user", 72 | "id": "4" 73 | }, 74 | "id": "2611726" 75 | }, 76 | { 77 | "created_time": "1296770323", 78 | "text": "o_hai", 79 | "from": { 80 | "username": "mikeyk", 81 | "first_name": "Mike", 82 | "last_name": "Krieger!!", 83 | "type": "user", 84 | "id": "4" 85 | }, 86 | "id": "2611747" 87 | } 88 | ] 89 | }, 90 | "caption": null, 91 | "link": "http://localhost:8000/p/E7/", 92 | "likes": { 93 | "count": 3 94 | }, 95 | "created_time": "1280379741", 96 | "images": { 97 | "low_resolution": { 98 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/07/28/84967fafe5f44b43a133ebabdd1f89a2_6.jpg", 99 | "width": 480, 100 | "height": 480 101 | }, 102 | "thumbnail": { 103 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/07/28/84967fafe5f44b43a133ebabdd1f89a2_5.jpg", 104 | "width": 150, 105 | "height": 150 106 | }, 107 | "standard_resolution": { 108 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/07/28/84967fafe5f44b43a133ebabdd1f89a2_7.jpg", 109 | "width": 612, 110 | "height": 612 111 | } 112 | }, 113 | "user_has_liked": true, 114 | "id": "315", 115 | "user": { 116 | "username": "mikeyk", 117 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_4_75sq_1292324747_debug.jpg", 118 | "id": "4" 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /fixtures/user_recent_media.json: -------------------------------------------------------------------------------- 1 | { 2 | "pagination": { 3 | "next_url": "http://localhost:8000/publicapi/v1/users/4/media/recent?q=iphone&lat=37.771&access_token=DEBUG&lng=-122.221&max_id=3382522&count=1", 4 | "next_max_id": 3382522 5 | }, 6 | "meta": { 7 | "code": 200 8 | }, 9 | "data": [ 10 | { 11 | "type": "image", 12 | "tags": [], 13 | "location": { 14 | "latitude": 37.781109999999998, 15 | "id": null, 16 | "longitude": -122.3947, 17 | "name": null 18 | }, 19 | "comments": { 20 | "count": 1, 21 | "data": [ 22 | { 23 | "created_time": "1296713291", 24 | "text": "Earear", 25 | "from": { 26 | "username": "mikeyk", 27 | "first_name": "Mike", 28 | "last_name": "Krieger!!", 29 | "type": "user", 30 | "id": "4" 31 | }, 32 | "id": "2611728" 33 | } 34 | ] 35 | }, 36 | "caption": { 37 | "created_time": "1296713291", 38 | "text": "Earear", 39 | "from": { 40 | "username": "mikeyk", 41 | "first_name": "Mike", 42 | "last_name": "Krieger!!", 43 | "type": "user", 44 | "id": "4" 45 | }, 46 | "id": "2611728" 47 | }, 48 | "link": "http://localhost:8000/p/M5z6/", 49 | "likes": { 50 | "count": 0 51 | }, 52 | "created_time": "1296713289", 53 | "images": { 54 | "low_resolution": { 55 | "url": "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/1ce5f3f490a640ca9068e6000c91adc5_6.jpg", 56 | "width": 480, 57 | "height": 480 58 | }, 59 | "thumbnail": { 60 | "url": "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/1ce5f3f490a640ca9068e6000c91adc5_5.jpg", 61 | "width": 150, 62 | "height": 150 63 | }, 64 | "standard_resolution": { 65 | "url": "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/1ce5f3f490a640ca9068e6000c91adc5_7.jpg", 66 | "width": 612, 67 | "height": 612 68 | } 69 | }, 70 | "user_has_liked": false, 71 | "id": "3382522", 72 | "user": { 73 | "username": "mikeyk", 74 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_4_75sq_1292324747_debug.jpg", 75 | "id": "4" 76 | } 77 | }, 78 | { 79 | "type": "video", 80 | "videos": { 81 | "low_resolution": { 82 | "url": "http://distilleryvesper9-13.ak.instagram.com/090d06dad9cd11e2aa0912313817975d_102.mp4", 83 | "width": 480, 84 | "height": 480 85 | }, 86 | "standard_resolution": { 87 | "url": "http://distilleryvesper9-13.ak.instagram.com/090d06dad9cd11e2aa0912313817975d_101.mp4", 88 | "width": 640, 89 | "height": 640 90 | }}, 91 | "users_in_photo": null, 92 | "filter": "Vesper", 93 | "tags": [], 94 | "comments": { 95 | "data": [{ 96 | "created_time": "1279332030", 97 | "text": "Love the sign here", 98 | "from": { 99 | "username": "mikeyk", 100 | "full_name": "Mikey Krieger", 101 | "id": "4", 102 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1242695_75sq_1293915800.jpg" 103 | }, 104 | "id": "8" 105 | }, 106 | { 107 | "created_time": "1279341004", 108 | "text": "Chilako taco", 109 | "from": { 110 | "username": "kevin", 111 | "full_name": "Kevin S", 112 | "id": "3", 113 | "profile_picture": "..." 114 | }, 115 | "id": "3" 116 | }], 117 | "count": 2 118 | }, 119 | "caption": null, 120 | "likes": { 121 | "count": 1, 122 | "data": [{ 123 | "username": "mikeyk", 124 | "full_name": "Mikeyk", 125 | "id": "4", 126 | "profile_picture": "..." 127 | }] 128 | }, 129 | "link": "http://instagr.am/p/D/", 130 | "user": { 131 | "username": "kevin", 132 | "full_name": "Kevin S", 133 | "profile_picture": "...", 134 | "bio": "...", 135 | "website": "...", 136 | "id": "3" 137 | }, 138 | "created_time": "1279340983", 139 | "images": { 140 | "low_resolution": { 141 | "url": "http://distilleryimage2.ak.instagram.com/11f75f1cd9cc11e2a0fd22000aa8039a_6.jpg", 142 | "width": 306, 143 | "height": 306 144 | }, 145 | "thumbnail": { 146 | "url": "http://distilleryimage2.ak.instagram.com/11f75f1cd9cc11e2a0fd22000aa8039a_5.jpg", 147 | "width": 150, 148 | "height": 150 149 | }, 150 | "standard_resolution": { 151 | "url": "http://distilleryimage2.ak.instagram.com/11f75f1cd9cc11e2a0fd22000aa8039a_7.jpg", 152 | "width": 612, 153 | "height": 612 154 | } 155 | }, 156 | "id": "3", 157 | "location": null 158 | } 159 | 160 | ] 161 | } 162 | -------------------------------------------------------------------------------- /fixtures/tag_recent_media.json: -------------------------------------------------------------------------------- 1 | { 2 | "pagination": {}, 3 | "meta": { 4 | "code": 200 5 | }, 6 | "data": [ 7 | { 8 | "type": "image", 9 | "tags": [], 10 | "location": null, 11 | "comments": { 12 | "count": 1, 13 | "data": [ 14 | { 15 | "created_time": "1287439973", 16 | "text": "Midnight dancing @kevinjaako. #iPhone #photography", 17 | "from": { 18 | "username": "cathycracks", 19 | "id": "12053" 20 | }, 21 | "id": "404609" 22 | } 23 | ] 24 | }, 25 | "caption": { 26 | "created_time": "1287439973", 27 | "text": "Midnight dancing @kevinjaako. #iPhone #photography", 28 | "from": { 29 | "username": "cathycracks", 30 | "id": "12053" 31 | }, 32 | "id": "404609" 33 | }, 34 | "link": "http://localhost:8000/p/Cbg2/", 35 | "likes": { 36 | "count": 0 37 | }, 38 | "created_time": "1287439896", 39 | "images": { 40 | "low_resolution": { 41 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/18/f9d2708b47c54949b9bb48ed676f8d8e_6.jpg", 42 | "width": 480, 43 | "height": 480 44 | }, 45 | "thumbnail": { 46 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/18/f9d2708b47c54949b9bb48ed676f8d8e_5.jpg", 47 | "width": 150, 48 | "height": 150 49 | }, 50 | "standard_resolution": { 51 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/18/f9d2708b47c54949b9bb48ed676f8d8e_7.jpg", 52 | "width": 612, 53 | "height": 612 54 | } 55 | }, 56 | "user_has_liked": false, 57 | "id": "636982", 58 | "user": { 59 | "username": "cathycracks", 60 | "first_name": "Cathy", 61 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_12053_75sq_1287858628.jpg", 62 | "id": "12053", 63 | "last_name": "Wang" 64 | } 65 | }, 66 | { 67 | "type": "image", 68 | "tags": [], 69 | "location": null, 70 | "comments": { 71 | "count": 1, 72 | "data": [ 73 | { 74 | "created_time": "1287237712", 75 | "text": "Van wie is eigenlijk mijn #iPhone", 76 | "from": { 77 | "username": "edwardvanmeel", 78 | "id": "46088" 79 | }, 80 | "id": "283284" 81 | } 82 | ] 83 | }, 84 | "caption": { 85 | "created_time": "1287237712", 86 | "text": "Van wie is eigenlijk mijn #iPhone", 87 | "from": { 88 | "username": "edwardvanmeel", 89 | "id": "46088" 90 | }, 91 | "id": "283284" 92 | }, 93 | "link": "http://localhost:8000/p/BvOo/", 94 | "likes": { 95 | "count": 1 96 | }, 97 | "created_time": "1287237652", 98 | "images": { 99 | "low_resolution": { 100 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/16/8848d8d1b48e44f5b4fe8c86a2e4f949_6.jpg", 101 | "width": 480, 102 | "height": 480 103 | }, 104 | "thumbnail": { 105 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/16/8848d8d1b48e44f5b4fe8c86a2e4f949_5.jpg", 106 | "width": 150, 107 | "height": 150 108 | }, 109 | "standard_resolution": { 110 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/16/8848d8d1b48e44f5b4fe8c86a2e4f949_7.jpg", 111 | "width": 612, 112 | "height": 612 113 | } 114 | }, 115 | "user_has_liked": false, 116 | "id": "455592", 117 | "user": { 118 | "username": "edwardvanmeel", 119 | "first_name": "Edward", 120 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_46088_75sq_1286621654.jpg", 121 | "id": "46088", 122 | "last_name": "van Meel" 123 | } 124 | }, 125 | { 126 | "type": "image", 127 | "tags": [], 128 | "location": null, 129 | "comments": { 130 | "count": 2, 131 | "data": [ 132 | { 133 | "created_time": "1286395811", 134 | "text": "Front-page worthy: @instagramapp. It's gorgeous. #iPhone", 135 | "from": { 136 | "username": "nik", 137 | "id": "5273" 138 | }, 139 | "id": "11380" 140 | }, 141 | { 142 | "created_time": "1286728380", 143 | "text": "I love this app too!", 144 | "from": { 145 | "username": "venetia", 146 | "id": "42294" 147 | }, 148 | "id": "86708" 149 | } 150 | ] 151 | }, 152 | "caption": { 153 | "created_time": "1286395811", 154 | "text": "Front-page worthy: @instagramapp. It's gorgeous. #iPhone", 155 | "from": { 156 | "username": "nik", 157 | "id": "5273" 158 | }, 159 | "id": "11380" 160 | }, 161 | "link": "http://localhost:8000/p/DBH/", 162 | "likes": { 163 | "count": 7 164 | }, 165 | "created_time": "1286394994", 166 | "images": { 167 | "low_resolution": { 168 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/06/ea33af4f6d5d4a5da569052e94d6b820_6.jpg", 169 | "width": 480, 170 | "height": 480 171 | }, 172 | "thumbnail": { 173 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/06/ea33af4f6d5d4a5da569052e94d6b820_5.jpg", 174 | "width": 150, 175 | "height": 150 176 | }, 177 | "standard_resolution": { 178 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/06/ea33af4f6d5d4a5da569052e94d6b820_7.jpg", 179 | "width": 612, 180 | "height": 612 181 | } 182 | }, 183 | "user_has_liked": false, 184 | "id": "12359", 185 | "user": { 186 | "username": "nik", 187 | "first_name": "nik", 188 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_5273_75sq_1286392268.jpg", 189 | "id": "5273", 190 | "last_name": "" 191 | } 192 | } 193 | ] 194 | } -------------------------------------------------------------------------------- /instagram/models.py: -------------------------------------------------------------------------------- 1 | from .helper import timestamp_to_datetime 2 | import six 3 | 4 | 5 | class ApiModel(object): 6 | 7 | @classmethod 8 | def object_from_dictionary(cls, entry): 9 | # make dict keys all strings 10 | if entry is None: 11 | return "" 12 | entry_str_dict = dict([(str(key), value) for key, value in entry.items()]) 13 | return cls(**entry_str_dict) 14 | 15 | def __repr__(self): 16 | return str(self) 17 | # if six.PY2: 18 | # return six.text_type(self).encode('utf8') 19 | # else: 20 | # return self.encode('utf8') 21 | 22 | def __str__(self): 23 | if six.PY3: 24 | return self.__unicode__() 25 | else: 26 | return unicode(self).encode('utf-8') 27 | 28 | 29 | class Image(ApiModel): 30 | 31 | def __init__(self, url, width, height): 32 | self.url = url 33 | self.height = height 34 | self.width = width 35 | 36 | def __unicode__(self): 37 | return "Image: %s" % self.url 38 | 39 | 40 | class Video(Image): 41 | 42 | def __unicode__(self): 43 | return "Video: %s" % self.url 44 | 45 | 46 | class Media(ApiModel): 47 | 48 | def __init__(self, id=None, **kwargs): 49 | self.id = id 50 | for key, value in six.iteritems(kwargs): 51 | setattr(self, key, value) 52 | 53 | def get_standard_resolution_url(self): 54 | if self.type == 'image': 55 | return self.images['standard_resolution'].url 56 | else: 57 | return self.videos['standard_resolution'].url 58 | 59 | def get_low_resolution_url(self): 60 | if self.type == 'image': 61 | return self.images['low_resolution'].url 62 | else: 63 | return self.videos['low_resolution'].url 64 | 65 | 66 | def get_thumbnail_url(self): 67 | return self.images['thumbnail'].url 68 | 69 | 70 | def __unicode__(self): 71 | return "Media: %s" % self.id 72 | 73 | @classmethod 74 | def object_from_dictionary(cls, entry): 75 | new_media = Media(id=entry['id']) 76 | new_media.type = entry['type'] 77 | 78 | new_media.user = User.object_from_dictionary(entry['user']) 79 | 80 | new_media.images = {} 81 | for version, version_info in six.iteritems(entry['images']): 82 | new_media.images[version] = Image.object_from_dictionary(version_info) 83 | 84 | if new_media.type == 'video': 85 | new_media.videos = {} 86 | for version, version_info in six.iteritems(entry['videos']): 87 | new_media.videos[version] = Video.object_from_dictionary(version_info) 88 | 89 | if 'user_has_liked' in entry: 90 | new_media.user_has_liked = entry['user_has_liked'] 91 | new_media.like_count = entry['likes']['count'] 92 | new_media.likes = [] 93 | if 'data' in entry['likes']: 94 | for like in entry['likes']['data']: 95 | new_media.likes.append(User.object_from_dictionary(like)) 96 | 97 | new_media.comment_count = entry['comments']['count'] 98 | new_media.comments = [] 99 | for comment in entry['comments']['data']: 100 | new_media.comments.append(Comment.object_from_dictionary(comment)) 101 | 102 | new_media.users_in_photo = [] 103 | if entry.get('users_in_photo'): 104 | for user_in_photo in entry['users_in_photo']: 105 | new_media.users_in_photo.append(UserInPhoto.object_from_dictionary(user_in_photo)) 106 | 107 | new_media.created_time = timestamp_to_datetime(entry['created_time']) 108 | 109 | if entry['location'] and 'id' in entry: 110 | new_media.location = Location.object_from_dictionary(entry['location']) 111 | 112 | new_media.caption = None 113 | if entry['caption']: 114 | new_media.caption = Comment.object_from_dictionary(entry['caption']) 115 | 116 | new_media.tags = [] 117 | if entry['tags']: 118 | for tag in entry['tags']: 119 | new_media.tags.append(Tag.object_from_dictionary({'name': tag})) 120 | 121 | new_media.link = entry['link'] 122 | 123 | new_media.filter = entry.get('filter') 124 | 125 | return new_media 126 | 127 | 128 | class MediaShortcode(Media): 129 | 130 | def __init__(self, shortcode=None, **kwargs): 131 | self.shortcode = shortcode 132 | for key, value in six.iteritems(kwargs): 133 | setattr(self, key, value) 134 | 135 | 136 | class Tag(ApiModel): 137 | def __init__(self, name, **kwargs): 138 | self.name = name 139 | for key, value in six.iteritems(kwargs): 140 | setattr(self, key, value) 141 | 142 | def __unicode__(self): 143 | return "Tag: %s" % self.name 144 | 145 | 146 | class Comment(ApiModel): 147 | def __init__(self, *args, **kwargs): 148 | for key, value in six.iteritems(kwargs): 149 | setattr(self, key, value) 150 | 151 | @classmethod 152 | def object_from_dictionary(cls, entry): 153 | user = User.object_from_dictionary(entry['from']) 154 | text = entry['text'] 155 | created_at = timestamp_to_datetime(entry['created_time']) 156 | id = entry['id'] 157 | return Comment(id=id, user=user, text=text, created_at=created_at) 158 | 159 | def __unicode__(self): 160 | return "Comment: %s said \"%s\"" % (self.user.username, self.text) 161 | 162 | 163 | class Point(ApiModel): 164 | def __init__(self, latitude, longitude): 165 | self.latitude = latitude 166 | self.longitude = longitude 167 | 168 | def __unicode__(self): 169 | return "Point: (%s, %s)" % (self.latitude, self.longitude) 170 | 171 | 172 | class Location(ApiModel): 173 | def __init__(self, id, *args, **kwargs): 174 | self.id = str(id) 175 | for key, value in six.iteritems(kwargs): 176 | setattr(self, key, value) 177 | 178 | @classmethod 179 | def object_from_dictionary(cls, entry): 180 | point = None 181 | if 'latitude' in entry: 182 | point = Point(entry.get('latitude'), 183 | entry.get('longitude')) 184 | location = Location(entry.get('id', 0), 185 | point=point, 186 | name=entry.get('name', '')) 187 | return location 188 | 189 | def __unicode__(self): 190 | return "Location: %s (%s)" % (self.id, self.point) 191 | 192 | 193 | class User(ApiModel): 194 | 195 | def __init__(self, id, *args, **kwargs): 196 | self.id = id 197 | for key, value in six.iteritems(kwargs): 198 | setattr(self, key, value) 199 | 200 | def __unicode__(self): 201 | return "User: %s" % self.username 202 | 203 | 204 | class Relationship(ApiModel): 205 | 206 | def __init__(self, incoming_status="none", outgoing_status="none", target_user_is_private=False): 207 | self.incoming_status = incoming_status 208 | self.outgoing_status = outgoing_status 209 | self.target_user_is_private = target_user_is_private 210 | 211 | def __unicode__(self): 212 | follows = False if self.outgoing_status == 'none' else True 213 | followed = False if self.incoming_status == 'none' else True 214 | 215 | return "Relationship: (Follows: %s, Followed by: %s)" % (follows, followed) 216 | 217 | 218 | class Position(ApiModel): 219 | def __init__(self, x, y): 220 | self.x = x 221 | self.y = y 222 | 223 | def __unicode__(self): 224 | return "Position: (%s, %s)" % (self.x, self.y) 225 | 226 | @classmethod 227 | def object_from_dictionary(cls, entry): 228 | if 'x' in entry: 229 | return Position(entry['x'], entry['y']) 230 | 231 | 232 | class UserInPhoto(ApiModel): 233 | def __init__(self, user, position): 234 | self.position = position 235 | self.user = user 236 | 237 | def __unicode__(self): 238 | return "UserInPhoto: (%s, %s)" % (self.user, self.position) 239 | 240 | @classmethod 241 | def object_from_dictionary(cls, entry): 242 | user = None 243 | if 'user' in entry: 244 | user = User.object_from_dictionary(entry['user']) 245 | 246 | if 'position' in entry: 247 | position = Position(entry['position']['x'], entry['position']['y']) 248 | 249 | return UserInPhoto(user, position) 250 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import types 4 | import six 5 | try: 6 | import simplejson as json 7 | except ImportError: 8 | import json 9 | import getpass 10 | import unittest 11 | from six.moves.urllib.parse import urlparse, parse_qs 12 | from instagram import client, oauth2, InstagramAPIError 13 | 14 | TEST_AUTH = False 15 | client_id = "DEBUG" 16 | client_secret = "DEBUG" 17 | access_token = "DEBUG" 18 | redirect_uri = "http://example.com" 19 | 20 | class MockHttp(object): 21 | 22 | def __init__(self, *args, **kwargs): 23 | pass 24 | 25 | def request(self, url, method="GET", body=None, headers={}): 26 | fail_state = { 27 | 'status':'400' 28 | }, "{}" 29 | 30 | parsed = urlparse(url) 31 | options = parse_qs(parsed.query) 32 | 33 | fn_name = str(active_call) 34 | if fn_name == 'get_authorize_login_url': 35 | return { 36 | 'status': '200', 37 | 'content-location':'http://example.com/redirect/login' 38 | }, None 39 | 40 | if not 'access_token' in options and not 'client_id' in options: 41 | fn_name += '_unauthorized' 42 | if 'self' in url and not 'access_token' in options: 43 | fn_name += '_no_auth_user' 44 | 45 | fl = open('fixtures/%s.json' % fn_name) 46 | content = fl.read() 47 | fl.close() 48 | json_content = json.loads(content) 49 | status = json_content['meta']['code'] 50 | return { 51 | 'status': status 52 | }, content 53 | 54 | oauth2.Http = MockHttp 55 | 56 | active_call = None 57 | class TestInstagramAPI(client.InstagramAPI): 58 | def __getattribute__(self, attr): 59 | global active_call 60 | actual_val = super(TestInstagramAPI, self).__getattribute__(attr) 61 | if isinstance(actual_val, types.MethodType): 62 | active_call = attr 63 | return actual_val 64 | 65 | class InstagramAuthTests(unittest.TestCase): 66 | def setUp(self): 67 | self.unauthenticated_api = TestInstagramAPI(client_id=client_id, redirect_uri=redirect_uri, client_secret=client_secret) 68 | 69 | def test_authorize_login_url(self): 70 | redirect_uri = self.unauthenticated_api.get_authorize_login_url() 71 | assert redirect_uri 72 | print("Please visit and authorize at:\n%s" % redirect_uri) 73 | code = raw_input("Paste received code (blank to skip): ").strip() 74 | if not code: 75 | return 76 | 77 | access_token = self.unauthenticated_api.exchange_code_for_access_token(code) 78 | assert access_token 79 | 80 | def test_xauth_exchange(self): 81 | """ Your client ID must be authorized for xAuth access; email 82 | xauth@instagram.com for access""" 83 | username = raw_input("Enter username for XAuth (blank to skip): ").strip() 84 | if not username: 85 | return 86 | password = getpass.getpass("Enter password for XAuth (blank to skip): ").strip() 87 | access_token = self.unauthenticated_api.exchange_xauth_login_for_access_token(username, password) 88 | assert access_token 89 | 90 | class InstagramAPITests(unittest.TestCase): 91 | 92 | def setUp(self): 93 | super(InstagramAPITests, self).setUp() 94 | self.client_only_api = TestInstagramAPI(client_id=client_id) 95 | self.api = TestInstagramAPI(access_token=access_token) 96 | 97 | def test_media_popular(self): 98 | self.api.media_popular(count=10) 99 | 100 | def test_media_search(self): 101 | self.client_only_api.media_search(lat=37.7,lng=-122.22) 102 | self.api.media_search(lat=37.7,lng=-122.22) 103 | 104 | def test_media_shortcode(self): 105 | self.client_only_api.media_shortcode('os1NQjxtvF') 106 | self.api.media_shortcode('os1NQjxtvF') 107 | 108 | def test_media_likes(self): 109 | self.client_only_api.media_likes(media_id=4) 110 | 111 | def test_like_media(self): 112 | self.api.like_media(media_id=4) 113 | self.api.unlike_media(media_id=4) 114 | 115 | """ 116 | TEMP; disabled this test while we add 117 | a proper response to create_media_comment 118 | def test_comment_media(self): 119 | comment = self.api.create_media_comment(media_id=4, text='test') 120 | self.api.delete_comment(media_id=4, comment_id=comment.id) 121 | """ 122 | 123 | def test_user_feed(self): 124 | self.api.user_media_feed(count=50) 125 | 126 | def test_generator_user_feed(self): 127 | generator = self.api.user_media_feed(as_generator=True, max_pages=3, count=2) 128 | for page in generator: 129 | str(generator) 130 | 131 | def test_generator_user_feed_all(self): 132 | generator = self.api.user_media_feed(as_generator=True, max_pages=None) 133 | for i in range(10): 134 | page = six.advance_iterator(generator) 135 | str(generator) 136 | 137 | generator = self.api.user_media_feed(as_generator=True, max_pages=0) 138 | for page in generator: 139 | assert False 140 | 141 | def test_user_liked_media(self): 142 | self.api.user_liked_media(count=10) 143 | 144 | def test_user_recent_media(self): 145 | media, url = self.api.user_recent_media(count=10) 146 | 147 | self.assertTrue( all( [hasattr(obj, 'type') for obj in media] ) ) 148 | 149 | image = media[0] 150 | self.assertEqual( 151 | image.get_standard_resolution_url(), 152 | "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/1ce5f3f490a640ca9068e6000c91adc5_7.jpg") 153 | 154 | self.assertEqual( 155 | image.get_low_resolution_url(), 156 | "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/1ce5f3f490a640ca9068e6000c91adc5_6.jpg") 157 | 158 | self.assertEqual( 159 | image.get_thumbnail_url(), 160 | "http://distillery-dev.s3.amazonaws.com/media/2011/02/02/1ce5f3f490a640ca9068e6000c91adc5_5.jpg") 161 | 162 | self.assertEqual( False, hasattr(image, 'videos') ) 163 | 164 | video = media[1] 165 | self.assertEqual( 166 | video.get_standard_resolution_url(), 167 | video.videos['standard_resolution'].url) 168 | 169 | self.assertEqual( 170 | video.get_standard_resolution_url(), 171 | "http://distilleryvesper9-13.ak.instagram.com/090d06dad9cd11e2aa0912313817975d_101.mp4") 172 | 173 | self.assertEqual( 174 | video.get_low_resolution_url(), 175 | "http://distilleryvesper9-13.ak.instagram.com/090d06dad9cd11e2aa0912313817975d_102.mp4") 176 | 177 | self.assertEqual( 178 | video.get_thumbnail_url(), 179 | "http://distilleryimage2.ak.instagram.com/11f75f1cd9cc11e2a0fd22000aa8039a_5.jpg") 180 | 181 | 182 | 183 | 184 | 185 | def test_user_search(self): 186 | self.api.user_search('mikeyk', 10) 187 | 188 | def test_user_follows(self): 189 | for page in self.api.user_followed_by(as_generator=True): 190 | str(page) 191 | 192 | def test_user_followed_by(self): 193 | for page in self.api.user_followed_by(as_generator=True): 194 | str(page) 195 | 196 | def test_other_user_followed_by(self): 197 | self.api.user_followed_by(user_id=3) 198 | 199 | def test_self_info(self): 200 | self.api.user() 201 | self.assertRaises(InstagramAPIError, self.client_only_api.user) 202 | 203 | def test_location_recent_media(self): 204 | self.api.location_recent_media(location_id=1) 205 | 206 | def test_location_search(self): 207 | self.api.location_search(lat=37.7,lng=-122.22, distance=2500) 208 | 209 | def test_location(self): 210 | self.api.location(1) 211 | 212 | def test_tag_recent_media(self): 213 | self.api.tag_recent_media(tag_name='1', count=5, max_tag_id='12345') 214 | 215 | def test_tag_recent_media_paginated(self): 216 | for page in self.api.tag_recent_media(tag_name='1', count=5, as_generator=True, max_pages=2): 217 | str(page) 218 | 219 | def test_tag_search(self): 220 | self.api.tag_search("coff") 221 | 222 | def test_tag(self): 223 | self.api.tag("coffee") 224 | 225 | def test_user_follows(self): 226 | self.api.user_follows() 227 | 228 | def test_user_followed_by(self): 229 | self.api.user_followed_by() 230 | 231 | def test_user_followed_by(self): 232 | self.api.user_followed_by() 233 | 234 | def test_user_requested_by(self): 235 | self.api.user_followed_by() 236 | 237 | def test_user_incoming_requests(self): 238 | self.api.user_incoming_requests() 239 | 240 | def test_change_relationship(self): 241 | self.api.change_user_relationship(user_id=10, action="follow") 242 | # test shortcuts as well 243 | self.api.follow_user(user_id='10') 244 | self.api.unfollow_user(user_id='10') 245 | 246 | def test_geography_recent_media(self): 247 | self.api.geography_recent_media(geography_id=1) 248 | 249 | if __name__ == '__main__': 250 | if not TEST_AUTH: 251 | del InstagramAuthTests 252 | 253 | unittest.main() 254 | -------------------------------------------------------------------------------- /instagram/bind.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | from .oauth2 import OAuth2Request 3 | import re 4 | from .json_import import simplejson 5 | import hmac 6 | from hashlib import sha256 7 | import six 8 | from six.moves.urllib.parse import quote 9 | import sys 10 | 11 | re_path_template = re.compile('{\w+}') 12 | 13 | 14 | def encode_string(value): 15 | return value.encode('utf-8') \ 16 | if isinstance(value, six.text_type) else str(value) 17 | 18 | 19 | class InstagramClientError(Exception): 20 | def __init__(self, error_message, status_code=None): 21 | self.status_code = status_code 22 | self.error_message = error_message 23 | 24 | def __str__(self): 25 | if self.status_code: 26 | return "(%s) %s" % (self.status_code, self.error_message) 27 | else: 28 | return self.error_message 29 | 30 | 31 | class InstagramAPIError(Exception): 32 | 33 | def __init__(self, status_code, error_type, error_message, *args, **kwargs): 34 | self.status_code = status_code 35 | self.error_type = error_type 36 | self.error_message = error_message 37 | 38 | def __str__(self): 39 | return "(%s) %s-%s" % (self.status_code, self.error_type, self.error_message) 40 | 41 | 42 | def bind_method(**config): 43 | 44 | class InstagramAPIMethod(object): 45 | 46 | path = config['path'] 47 | method = config.get('method', 'GET') 48 | accepts_parameters = config.get("accepts_parameters", []) 49 | signature = config.get("signature", False) 50 | requires_target_user = config.get('requires_target_user', False) 51 | paginates = config.get('paginates', False) 52 | root_class = config.get('root_class', None) 53 | response_type = config.get("response_type", "list") 54 | include_secret = config.get("include_secret", False) 55 | objectify_response = config.get("objectify_response", True) 56 | exclude_format = config.get('exclude_format', False) 57 | 58 | def __init__(self, api, *args, **kwargs): 59 | self.api = api 60 | self.as_generator = kwargs.pop("as_generator", False) 61 | if self.as_generator: 62 | self.pagination_format = 'next_url' 63 | else: 64 | self.pagination_format = kwargs.pop('pagination_format', 'next_url') 65 | self.return_json = kwargs.pop("return_json", False) 66 | self.max_pages = kwargs.pop("max_pages", 3) 67 | self.with_next_url = kwargs.pop("with_next_url", None) 68 | self.parameters = {} 69 | self._build_parameters(args, kwargs) 70 | self._build_path() 71 | 72 | def _build_parameters(self, args, kwargs): 73 | # via tweepy https://github.com/joshthecoder/tweepy/ 74 | for index, value in enumerate(args): 75 | if value is None: 76 | continue 77 | 78 | try: 79 | self.parameters[self.accepts_parameters[index]] = encode_string(value) 80 | except IndexError: 81 | raise InstagramClientError("Too many arguments supplied") 82 | 83 | for key, value in six.iteritems(kwargs): 84 | if value is None: 85 | continue 86 | if key in self.parameters: 87 | raise InstagramClientError("Parameter %s already supplied" % key) 88 | self.parameters[key] = encode_string(value) 89 | if 'user_id' in self.accepts_parameters and not 'user_id' in self.parameters \ 90 | and not self.requires_target_user: 91 | self.parameters['user_id'] = 'self' 92 | 93 | def _build_path(self): 94 | for variable in re_path_template.findall(self.path): 95 | name = variable.strip('{}') 96 | 97 | try: 98 | value = quote(self.parameters[name]) 99 | except KeyError: 100 | raise Exception('No parameter value found for path variable: %s' % name) 101 | del self.parameters[name] 102 | 103 | self.path = self.path.replace(variable, value) 104 | 105 | if self.api.format and not self.exclude_format: 106 | self.path = self.path + '.%s' % self.api.format 107 | 108 | def _build_pagination_info(self, content_obj): 109 | """Extract pagination information in the desired format.""" 110 | pagination = content_obj.get('pagination', {}) 111 | if self.pagination_format == 'next_url': 112 | return pagination.get('next_url') 113 | if self.pagination_format == 'dict': 114 | return pagination 115 | raise Exception('Invalid value for pagination_format: %s' % self.pagination_format) 116 | 117 | def _do_api_request(self, url, method="GET", body=None, headers=None): 118 | headers = headers or {} 119 | if self.signature and self.api.client_ips != None and self.api.client_secret != None: 120 | secret = self.api.client_secret 121 | ips = self.api.client_ips 122 | signature = hmac.new(secret, ips, sha256).hexdigest() 123 | headers['X-Insta-Forwarded-For'] = '|'.join([ips, signature]) 124 | 125 | response, content = OAuth2Request(self.api).make_request(url, method=method, body=body, headers=headers) 126 | if response['status'] == '503' or response['status'] == '429': 127 | raise InstagramAPIError(response['status'], "Rate limited", "Your client is making too many request per second") 128 | try: 129 | content_obj = simplejson.loads(content) 130 | except ValueError: 131 | raise InstagramClientError('Unable to parse response, not valid JSON.', status_code=response['status']) 132 | # Handle OAuthRateLimitExceeded from Instagram's Nginx which uses different format to documented api responses 133 | if 'meta' not in content_obj: 134 | if content_obj.get('code') == 420 or content_obj.get('code') == 429: 135 | error_message = content_obj.get('error_message') or "Your client is making too many request per second" 136 | raise InstagramAPIError(content_obj.get('code'), "Rate limited", error_message) 137 | raise InstagramAPIError(content_obj.get('code'), content_obj.get('error_type'), content_obj.get('error_message')) 138 | api_responses = [] 139 | status_code = content_obj['meta']['code'] 140 | self.api.x_ratelimit_remaining = response.get("x-ratelimit-remaining",None) 141 | self.api.x_ratelimit = response.get("x-ratelimit-limit",None) 142 | if status_code == 200: 143 | if not self.objectify_response: 144 | return content_obj, None 145 | 146 | if self.response_type == 'list': 147 | for entry in content_obj['data']: 148 | if self.return_json: 149 | api_responses.append(entry) 150 | else: 151 | obj = self.root_class.object_from_dictionary(entry) 152 | api_responses.append(obj) 153 | elif self.response_type == 'entry': 154 | data = content_obj['data'] 155 | if self.return_json: 156 | api_responses = data 157 | else: 158 | api_responses = self.root_class.object_from_dictionary(data) 159 | elif self.response_type == 'empty': 160 | pass 161 | return api_responses, self._build_pagination_info(content_obj) 162 | else: 163 | raise InstagramAPIError(status_code, content_obj['meta']['error_type'], content_obj['meta']['error_message']) 164 | 165 | def _paginator_with_url(self, url, method="GET", body=None, headers=None): 166 | headers = headers or {} 167 | pages_read = 0 168 | while url and (self.max_pages is None or pages_read < self.max_pages): 169 | api_responses, url = self._do_api_request(url, method, body, headers) 170 | pages_read += 1 171 | yield api_responses, url 172 | return 173 | 174 | def _get_with_next_url(self, url, method="GET", body=None, headers=None): 175 | headers = headers or {} 176 | content, next = self._do_api_request(url, method, body, headers) 177 | return content, next 178 | 179 | def execute(self): 180 | url, method, body, headers = OAuth2Request(self.api).prepare_request(self.method, 181 | self.path, 182 | self.parameters, 183 | include_secret=self.include_secret) 184 | if self.with_next_url: 185 | return self._get_with_next_url(self.with_next_url, method, body, headers) 186 | if self.as_generator: 187 | return self._paginator_with_url(url, method, body, headers) 188 | else: 189 | content, next = self._do_api_request(url, method, body, headers) 190 | if self.paginates: 191 | return content, next 192 | else: 193 | return content 194 | 195 | def _call(api, *args, **kwargs): 196 | method = InstagramAPIMethod(api, *args, **kwargs) 197 | return method.execute() 198 | 199 | return _call 200 | -------------------------------------------------------------------------------- /instagram/client.py: -------------------------------------------------------------------------------- 1 | from . import oauth2 2 | from .bind import bind_method 3 | from .models import MediaShortcode, Media, User, Location, Tag, Comment, Relationship 4 | 5 | MEDIA_ACCEPT_PARAMETERS = ["count", "max_id"] 6 | SEARCH_ACCEPT_PARAMETERS = ["q", "count"] 7 | 8 | SUPPORTED_FORMATS = ['json'] 9 | 10 | 11 | class InstagramAPI(oauth2.OAuth2API): 12 | 13 | host = "api.instagram.com" 14 | base_path = "/v1" 15 | access_token_field = "access_token" 16 | authorize_url = "https://api.instagram.com/oauth/authorize" 17 | access_token_url = "https://api.instagram.com/oauth/access_token" 18 | protocol = "https" 19 | api_name = "Instagram" 20 | x_ratelimit_remaining = None 21 | x_ratelimit = None 22 | 23 | def __init__(self, *args, **kwargs): 24 | format = kwargs.get('format', 'json') 25 | if format in SUPPORTED_FORMATS: 26 | self.format = format 27 | else: 28 | raise Exception("Unsupported format") 29 | super(InstagramAPI, self).__init__(**kwargs) 30 | 31 | media_popular = bind_method( 32 | path="/media/popular", 33 | accepts_parameters=MEDIA_ACCEPT_PARAMETERS, 34 | root_class=Media) 35 | 36 | media_search = bind_method( 37 | path="/media/search", 38 | accepts_parameters=SEARCH_ACCEPT_PARAMETERS + ['lat', 'lng', 'min_timestamp', 'max_timestamp', 'distance'], 39 | root_class=Media) 40 | 41 | media_shortcode = bind_method( 42 | path="/media/shortcode/{shortcode}", 43 | accepts_parameters=['shortcode'], 44 | response_type="entry", 45 | root_class=MediaShortcode, 46 | exclude_format=True) 47 | 48 | 49 | media_likes = bind_method( 50 | path="/media/{media_id}/likes", 51 | accepts_parameters=['media_id'], 52 | root_class=User) 53 | 54 | like_media = bind_method( 55 | path="/media/{media_id}/likes", 56 | method="POST", 57 | signature=True, 58 | accepts_parameters=['media_id'], 59 | response_type="empty") 60 | 61 | unlike_media = bind_method( 62 | path="/media/{media_id}/likes", 63 | method="DELETE", 64 | signature=True, 65 | accepts_parameters=['media_id'], 66 | response_type="empty") 67 | 68 | create_media_comment = bind_method( 69 | path="/media/{media_id}/comments", 70 | method="POST", 71 | signature=True, 72 | accepts_parameters=['media_id', 'text'], 73 | response_type="empty", 74 | root_class=Comment) 75 | 76 | delete_comment = bind_method( 77 | path="/media/{media_id}/comments/{comment_id}", 78 | method="DELETE", 79 | signature=True, 80 | accepts_parameters=['media_id', 'comment_id'], 81 | response_type="empty") 82 | 83 | media_comments = bind_method( 84 | path="/media/{media_id}/comments", 85 | method="GET", 86 | accepts_parameters=['media_id'], 87 | response_type="list", 88 | root_class=Comment) 89 | 90 | media = bind_method( 91 | path="/media/{media_id}", 92 | accepts_parameters=['media_id'], 93 | response_type="entry", 94 | root_class=Media) 95 | 96 | user_media_feed = bind_method( 97 | path="/users/self/feed", 98 | accepts_parameters=MEDIA_ACCEPT_PARAMETERS, 99 | root_class=Media, 100 | paginates=True) 101 | 102 | user_liked_media = bind_method( 103 | path="/users/self/media/liked", 104 | accepts_parameters=MEDIA_ACCEPT_PARAMETERS, 105 | root_class=Media, 106 | paginates=True) 107 | 108 | user_recent_media = bind_method( 109 | path="/users/{user_id}/media/recent", 110 | accepts_parameters=MEDIA_ACCEPT_PARAMETERS + ['user_id', 'min_id', 'max_timestamp', 'min_timestamp'], 111 | root_class=Media, 112 | paginates=True) 113 | 114 | user_search = bind_method( 115 | path="/users/search", 116 | accepts_parameters=SEARCH_ACCEPT_PARAMETERS, 117 | root_class=User) 118 | 119 | user_follows = bind_method( 120 | path="/users/{user_id}/follows", 121 | accepts_parameters=["user_id"], 122 | paginates=True, 123 | root_class=User) 124 | 125 | user_followed_by = bind_method( 126 | path="/users/{user_id}/followed-by", 127 | accepts_parameters=["user_id"], 128 | paginates=True, 129 | root_class=User) 130 | 131 | user = bind_method( 132 | path="/users/{user_id}", 133 | accepts_parameters=["user_id"], 134 | root_class=User, 135 | response_type="entry") 136 | 137 | location_recent_media = bind_method( 138 | path="/locations/{location_id}/media/recent", 139 | accepts_parameters=MEDIA_ACCEPT_PARAMETERS + ['location_id'], 140 | root_class=Media, 141 | paginates=True) 142 | 143 | location_search = bind_method( 144 | path="/locations/search", 145 | accepts_parameters=SEARCH_ACCEPT_PARAMETERS + ['lat', 'lng', 'foursquare_id', 'foursquare_v2_id'], 146 | root_class=Location) 147 | 148 | location = bind_method( 149 | path="/locations/{location_id}", 150 | accepts_parameters=["location_id"], 151 | root_class=Location, 152 | response_type="entry") 153 | 154 | geography_recent_media = bind_method( 155 | path="/geographies/{geography_id}/media/recent", 156 | accepts_parameters=MEDIA_ACCEPT_PARAMETERS + ["geography_id"], 157 | root_class=Media, 158 | paginates=True) 159 | 160 | tag_recent_media = bind_method( 161 | path="/tags/{tag_name}/media/recent", 162 | accepts_parameters=['count', 'max_tag_id', 'tag_name'], 163 | root_class=Media, 164 | paginates=True) 165 | 166 | tag_search = bind_method( 167 | path="/tags/search", 168 | accepts_parameters=SEARCH_ACCEPT_PARAMETERS, 169 | root_class=Tag, 170 | paginates=True) 171 | 172 | tag = bind_method( 173 | path="/tags/{tag_name}", 174 | accepts_parameters=["tag_name"], 175 | root_class=Tag, 176 | response_type="entry") 177 | 178 | user_incoming_requests = bind_method( 179 | path="/users/self/requested-by", 180 | root_class=User) 181 | 182 | change_user_relationship = bind_method( 183 | method="POST", 184 | path="/users/{user_id}/relationship", 185 | signature=True, 186 | root_class=Relationship, 187 | accepts_parameters=["user_id", "action"], 188 | paginates=True, 189 | requires_target_user=True, 190 | response_type="entry") 191 | 192 | user_relationship = bind_method( 193 | method="GET", 194 | path="/users/{user_id}/relationship", 195 | root_class=Relationship, 196 | accepts_parameters=["user_id"], 197 | paginates=False, 198 | requires_target_user=True, 199 | response_type="entry") 200 | 201 | def _make_relationship_shortcut(action): 202 | def _inner(self, *args, **kwargs): 203 | return self.change_user_relationship(user_id=kwargs.get("user_id"), 204 | action=action) 205 | return _inner 206 | 207 | follow_user = _make_relationship_shortcut('follow') 208 | unfollow_user = _make_relationship_shortcut('unfollow') 209 | block_user = _make_relationship_shortcut('block') 210 | unblock_user = _make_relationship_shortcut('unblock') 211 | approve_user_request = _make_relationship_shortcut('approve') 212 | ignore_user_request = _make_relationship_shortcut('ignore') 213 | 214 | def _make_subscription_action(method, include=None, exclude=None): 215 | accepts_parameters = ["object", 216 | "aspect", 217 | "object_id", # Optional if subscribing to all users 218 | "callback_url", 219 | "lat", # Geography 220 | "lng", # Geography 221 | "radius", # Geography 222 | "verify_token"] 223 | 224 | if include: 225 | accepts_parameters.extend(include) 226 | if exclude: 227 | accepts_parameters = [x for x in accepts_parameters if x not in exclude] 228 | signature = False if method == 'GET' else True 229 | return bind_method( 230 | path="/subscriptions", 231 | method=method, 232 | accepts_parameters=accepts_parameters, 233 | include_secret=True, 234 | objectify_response=False, 235 | signature=signature, 236 | ) 237 | 238 | create_subscription = _make_subscription_action('POST') 239 | list_subscriptions = _make_subscription_action('GET') 240 | delete_subscriptions = _make_subscription_action('DELETE', exclude=['object_id'], include=['id']) 241 | -------------------------------------------------------------------------------- /instagram/oauth2.py: -------------------------------------------------------------------------------- 1 | from .json_import import simplejson 2 | from six.moves.urllib.parse import urlencode 3 | from httplib2 import Http 4 | from hashlib import sha256 5 | import mimetypes 6 | import six 7 | import hmac 8 | 9 | 10 | class OAuth2AuthExchangeError(Exception): 11 | def __init__(self, description): 12 | self.description = description 13 | 14 | def __str__(self): 15 | return self.description 16 | 17 | 18 | class OAuth2API(object): 19 | host = None 20 | base_path = None 21 | authorize_url = None 22 | access_token_url = None 23 | redirect_uri = None 24 | # some providers use "oauth_token" 25 | access_token_field = "access_token" 26 | protocol = "https" 27 | # override with 'Instagram', etc 28 | api_name = "Generic API" 29 | 30 | def __init__(self, client_id=None, client_secret=None, client_ips=None, access_token=None, redirect_uri=None): 31 | self.client_id = client_id 32 | self.client_secret = client_secret 33 | self.client_ips = client_ips 34 | self.access_token = access_token 35 | self.redirect_uri = redirect_uri 36 | 37 | def get_authorize_url(self, scope=None): 38 | req = OAuth2AuthExchangeRequest(self) 39 | return req.get_authorize_url(scope=scope) 40 | 41 | def get_authorize_login_url(self, scope=None): 42 | """ scope should be a tuple or list of requested scope access levels """ 43 | req = OAuth2AuthExchangeRequest(self) 44 | return req.get_authorize_login_url(scope=scope) 45 | 46 | def exchange_code_for_access_token(self, code): 47 | req = OAuth2AuthExchangeRequest(self) 48 | return req.exchange_for_access_token(code=code) 49 | 50 | def exchange_user_id_for_access_token(self, user_id): 51 | req = OAuth2AuthExchangeRequest(self) 52 | return req.exchange_for_access_token(user_id=user_id) 53 | 54 | def exchange_xauth_login_for_access_token(self, username, password, scope=None): 55 | """ scope should be a tuple or list of requested scope access levels """ 56 | req = OAuth2AuthExchangeRequest(self) 57 | return req.exchange_for_access_token(username=username, password=password, 58 | scope=scope) 59 | 60 | 61 | class OAuth2AuthExchangeRequest(object): 62 | def __init__(self, api): 63 | self.api = api 64 | 65 | def _url_for_authorize(self, scope=None): 66 | client_params = { 67 | "client_id": self.api.client_id, 68 | "response_type": "code", 69 | "redirect_uri": self.api.redirect_uri 70 | } 71 | if scope: 72 | client_params.update(scope=' '.join(scope)) 73 | url_params = urlencode(client_params) 74 | return "%s?%s" % (self.api.authorize_url, url_params) 75 | 76 | def _data_for_exchange(self, code=None, username=None, password=None, scope=None, user_id=None): 77 | client_params = { 78 | "client_id": self.api.client_id, 79 | "client_secret": self.api.client_secret, 80 | "redirect_uri": self.api.redirect_uri, 81 | "grant_type": "authorization_code" 82 | } 83 | if code: 84 | client_params.update(code=code) 85 | elif username and password: 86 | client_params.update(username=username, 87 | password=password, 88 | grant_type="password") 89 | if scope: 90 | client_params.update(scope=' '.join(scope)) 91 | elif user_id: 92 | client_params.update(user_id=user_id) 93 | return urlencode(client_params) 94 | 95 | def get_authorize_url(self, scope=None): 96 | return self._url_for_authorize(scope=scope) 97 | 98 | def get_authorize_login_url(self, scope=None): 99 | http_object = Http(disable_ssl_certificate_validation=True) 100 | 101 | url = self._url_for_authorize(scope=scope) 102 | response, content = http_object.request(url) 103 | if response['status'] != '200': 104 | raise OAuth2AuthExchangeError("The server returned a non-200 response for URL %s" % url) 105 | redirected_to = response['content-location'] 106 | return redirected_to 107 | 108 | def exchange_for_access_token(self, code=None, username=None, password=None, scope=None, user_id=None): 109 | data = self._data_for_exchange(code, username, password, scope=scope, user_id=user_id) 110 | http_object = Http(disable_ssl_certificate_validation=True) 111 | url = self.api.access_token_url 112 | response, content = http_object.request(url, method="POST", body=data) 113 | parsed_content = simplejson.loads(content.decode()) 114 | if int(response['status']) != 200: 115 | raise OAuth2AuthExchangeError(parsed_content.get("error_message", "")) 116 | return parsed_content['access_token'], parsed_content['user'] 117 | 118 | 119 | class OAuth2Request(object): 120 | def __init__(self, api): 121 | self.api = api 122 | 123 | def _generate_sig(self, endpoint, params, secret): 124 | sig = endpoint 125 | for key in sorted(params.keys()): 126 | sig += '|%s=%s' % (key, params[key]) 127 | return hmac.new(secret.encode(), sig.encode(), sha256).hexdigest() 128 | 129 | def url_for_get(self, path, parameters): 130 | return self._full_url_with_params(path, parameters) 131 | 132 | def get_request(self, path, **kwargs): 133 | return self.make_request(self.prepare_request("GET", path, kwargs)) 134 | 135 | def post_request(self, path, **kwargs): 136 | return self.make_request(self.prepare_request("POST", path, kwargs)) 137 | 138 | def _full_url(self, path, include_secret=False, include_signed_request=True): 139 | return "%s://%s%s%s%s%s" % (self.api.protocol, 140 | self.api.host, 141 | self.api.base_path, 142 | path, 143 | self._auth_query(include_secret), 144 | self._signed_request(path, {}, include_signed_request, include_secret)) 145 | 146 | def _full_url_with_params(self, path, params, include_secret=False, include_signed_request=True): 147 | return (self._full_url(path, include_secret) + 148 | self._full_query_with_params(params) + 149 | self._signed_request(path, params, include_signed_request, include_secret)) 150 | 151 | def _full_query_with_params(self, params): 152 | params = ("&" + urlencode(params)) if params else "" 153 | return params 154 | 155 | def _auth_query(self, include_secret=False): 156 | if self.api.access_token: 157 | return ("?%s=%s" % (self.api.access_token_field, self.api.access_token)) 158 | elif self.api.client_id: 159 | base = ("?client_id=%s" % (self.api.client_id)) 160 | if include_secret: 161 | base += "&client_secret=%s" % (self.api.client_secret) 162 | return base 163 | 164 | def _signed_request(self, path, params, include_signed_request, include_secret): 165 | if include_signed_request and self.api.client_secret is not None: 166 | if self.api.access_token: 167 | params['access_token'] = self.api.access_token 168 | elif self.api.client_id: 169 | params['client_id'] = self.api.client_id 170 | if include_secret and self.api.client_secret: 171 | params['client_secret'] = self.api.client_secret 172 | return "&sig=%s" % self._generate_sig(path, params, self.api.client_secret) 173 | else: 174 | return '' 175 | 176 | def _post_body(self, params): 177 | return urlencode(params) 178 | 179 | def _encode_multipart(self, params, files): 180 | boundary = "MuL7Ip4rt80uND4rYF0o" 181 | 182 | def get_content_type(file_name): 183 | return mimetypes.guess_type(file_name)[0] or "application/octet-stream" 184 | 185 | def encode_field(field_name): 186 | return ("--" + boundary, 187 | 'Content-Disposition: form-data; name="%s"' % (field_name), 188 | "", str(params[field_name])) 189 | 190 | def encode_file(field_name): 191 | file_name, file_handle = files[field_name] 192 | return ("--" + boundary, 193 | 'Content-Disposition: form-data; name="%s"; filename="%s"' % (field_name, file_name), 194 | "Content-Type: " + get_content_type(file_name), 195 | "", file_handle.read()) 196 | 197 | lines = [] 198 | for field in params: 199 | lines.extend(encode_field(field)) 200 | for field in files: 201 | lines.extend(encode_file(field)) 202 | lines.extend(("--%s--" % (boundary), "")) 203 | body = "\r\n".join(lines) 204 | 205 | headers = {"Content-Type": "multipart/form-data; boundary=" + boundary, 206 | "Content-Length": str(len(body))} 207 | 208 | return body, headers 209 | 210 | def prepare_and_make_request(self, method, path, params, include_secret=False): 211 | url, method, body, headers = self.prepare_request(method, path, params, include_secret) 212 | return self.make_request(url, method, body, headers) 213 | 214 | def prepare_request(self, method, path, params, include_secret=False): 215 | url = body = None 216 | headers = {} 217 | 218 | if not params.get('files'): 219 | if method == "POST": 220 | body = self._post_body(params) 221 | headers = {'Content-type': 'application/x-www-form-urlencoded'} 222 | url = self._full_url(path, include_secret) 223 | else: 224 | url = self._full_url_with_params(path, params, include_secret) 225 | else: 226 | body, headers = self._encode_multipart(params, params['files']) 227 | url = self._full_url(path) 228 | 229 | return url, method, body, headers 230 | 231 | def make_request(self, url, method="GET", body=None, headers=None): 232 | headers = headers or {} 233 | if not 'User-Agent' in headers: 234 | headers.update({"User-Agent": "%s Python Client" % self.api.api_name}) 235 | # https://github.com/jcgregorio/httplib2/issues/173 236 | # bug in httplib2 w/ Python 3 and disable_ssl_certificate_validation=True 237 | http_obj = Http() if six.PY3 else Http(disable_ssl_certificate_validation=True) 238 | return http_obj.request(url, method, body=body, headers=headers) 239 | -------------------------------------------------------------------------------- /fixtures/user_follows.json: -------------------------------------------------------------------------------- 1 | { 2 | "pagination": { 3 | "next_url": "http://localhost:8000/publicapi/v1/users/4/follows/users?q=iphone&lat=37.771&cursor=1201686&access_token=DEBUG&lng=-122.221", 4 | "next_cursor": "1201686" 5 | }, 6 | "meta": { 7 | "code": 200 8 | }, 9 | "data": [ 10 | { 11 | "username": "kevin", 12 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_3_75sq_1295493319_debug.jpg", 13 | "id": "3" 14 | }, 15 | { 16 | "username": "moop55_ok", 17 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_332654_75sq_1294963309_debug.jpg", 18 | "id": "332654" 19 | }, 20 | { 21 | "username": "ooop", 22 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_580183_75sq_1295562975_debug.jpg", 23 | "id": "580183" 24 | }, 25 | { 26 | "username": "tastyboo", 27 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_580178_75sq_1295490998_debug.jpg", 28 | "id": "580178" 29 | }, 30 | { 31 | "username": "moopafa", 32 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 33 | "id": "580174" 34 | }, 35 | { 36 | "username": "jalter", 37 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.png", 38 | "id": "51" 39 | }, 40 | { 41 | "username": "doug", 42 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_17_75sq_1287382013.jpg", 43 | "id": "17" 44 | }, 45 | { 46 | "username": "danman", 47 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_139329_75sq_1287670462.jpg", 48 | "id": "139329" 49 | }, 50 | { 51 | "username": "amine", 52 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_136_75sq_1286357298.jpg", 53 | "id": "136" 54 | }, 55 | { 56 | "username": "moopevil3", 57 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 58 | "id": "580164" 59 | }, 60 | { 61 | "username": "kevnull", 62 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_12169_75sq_1286433153.jpg", 63 | "id": "12169" 64 | }, 65 | { 66 | "username": "wymp", 67 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 68 | "id": "402370" 69 | }, 70 | { 71 | "username": "rad", 72 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_330307_75sq_1288842417.jpg", 73 | "id": "330307" 74 | }, 75 | { 76 | "username": "mpreysman", 77 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_2431_75sq_1286378624.jpg", 78 | "id": "2431" 79 | }, 80 | { 81 | "username": "joulee", 82 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_354221_75sq_1288477666.jpg", 83 | "id": "354221" 84 | }, 85 | { 86 | "username": "drtyhbo", 87 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 88 | "id": "451846" 89 | }, 90 | { 91 | "username": "pm", 92 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 93 | "id": "21017" 94 | }, 95 | { 96 | "username": "mike_matas", 97 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 98 | "id": "283400" 99 | }, 100 | { 101 | "username": "joehewitt", 102 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 103 | "id": "340483" 104 | }, 105 | { 106 | "username": "itod", 107 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_73142_75sq_1288712029.jpg", 108 | "id": "73142" 109 | }, 110 | { 111 | "username": "erickschonfeld", 112 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_412333_75sq_1288708924.jpg", 113 | "id": "412333" 114 | }, 115 | { 116 | "username": "edog1203", 117 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 118 | "id": "334077" 119 | }, 120 | { 121 | "username": "addumb", 122 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_156856_75sq_1287287045.jpg", 123 | "id": "156856" 124 | }, 125 | { 126 | "username": "neo121", 127 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_4124_75sq_1286682619.jpg", 128 | "id": "4124" 129 | }, 130 | { 131 | "username": "heathsplosion", 132 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_4962_75sq_1287559547.jpg", 133 | "id": "4962" 134 | }, 135 | { 136 | "username": "baristaskills", 137 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_400745_75sq_1288898161.jpg", 138 | "id": "400745" 139 | }, 140 | { 141 | "username": "mephaust", 142 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 143 | "id": "379207" 144 | }, 145 | { 146 | "username": "zuck", 147 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 148 | "id": "314216" 149 | }, 150 | { 151 | "username": "kikife", 152 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_89613_75sq_1288524236.jpg", 153 | "id": "89613" 154 | }, 155 | { 156 | "username": "nathanfolkman", 157 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_10331_75sq_1288543942.jpg", 158 | "id": "10331" 159 | }, 160 | { 161 | "username": "szetopia", 162 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_324431_75sq_1288324508.jpg", 163 | "id": "324431" 164 | }, 165 | { 166 | "username": "savasavasava", 167 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_230975_75sq_1288882103.jpg", 168 | "id": "230975" 169 | }, 170 | { 171 | "username": "marcoarment", 172 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_319323_75sq_1288295580.jpg", 173 | "id": "319323" 174 | }, 175 | { 176 | "username": "jonathan", 177 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_104_75sq_1286247931.jpg", 178 | "id": "104" 179 | }, 180 | { 181 | "username": "marcin", 182 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_3436_75sq_1286398236.jpg", 183 | "id": "3436" 184 | }, 185 | { 186 | "username": "sara", 187 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_3843_75sq_1286396157.jpg", 188 | "id": "3843" 189 | }, 190 | { 191 | "username": "garrytan", 192 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_118666_75sq_1287710518.jpg", 193 | "id": "118666" 194 | }, 195 | { 196 | "username": "bijan", 197 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_4415_75sq_1286388941.jpg", 198 | "id": "4415" 199 | }, 200 | { 201 | "username": "kevinrose", 202 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_124163_75sq_1287092509.jpg", 203 | "id": "124163" 204 | }, 205 | { 206 | "username": "joshjohnson", 207 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_98476_75sq_1288321499.jpg", 208 | "id": "98476" 209 | }, 210 | { 211 | "username": "andreklbacelar", 212 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_9456_75sq_1287715249.jpg", 213 | "id": "9456" 214 | }, 215 | { 216 | "username": "sacca", 217 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_221307_75sq_1287732175.jpg", 218 | "id": "221307" 219 | }, 220 | { 221 | "username": "suzykrieger", 222 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_220658_75sq_1287968389.jpg", 223 | "id": "220658" 224 | }, 225 | { 226 | "username": "nandakbp", 227 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_269007_75sq_1287970385.jpg", 228 | "id": "269007" 229 | }, 230 | { 231 | "username": "jpollock", 232 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 233 | "id": "157011" 234 | }, 235 | { 236 | "username": "iphonequeen", 237 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_6392_75sq_1286477168.jpg", 238 | "id": "6392" 239 | }, 240 | { 241 | "username": "igutfreund", 242 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 243 | "id": "248626" 244 | }, 245 | { 246 | "username": "onwall", 247 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 248 | "id": "248299" 249 | }, 250 | { 251 | "username": "missmallibu", 252 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_235309_75sq_1287796262.jpg", 253 | "id": "235309" 254 | }, 255 | { 256 | "username": "jimgoldstein", 257 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_171983_75sq_1287376290.jpg", 258 | "id": "171983" 259 | } 260 | ] 261 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | - - - 2 | 3 | **_This project is not actively maintained. Proceed at your own risk!_** 4 | 5 | - - - 6 | 7 | [![Build Status](https://api.travis-ci.org/Instagram/python-instagram.svg)](https://travis-ci.org/Instagram/python-instagram) 8 | 9 | python-instagram 10 | ====== 11 | A Python 2/3 client for the Instagram REST and Search APIs 12 | 13 | Installation 14 | ----- 15 | ``` 16 | pip install python-instagram 17 | ``` 18 | Requires 19 | ----- 20 | * httplib2 21 | * simplejson 22 | * six 23 | 24 | 25 | Instagram REST and Search APIs 26 | ------------------------------ 27 | Our [developer site](http://instagram.com/developer) documents all the Instagram REST and Search APIs. 28 | 29 | 30 | Blog 31 | ---------------------------- 32 | The [Developer Blog] features news and important announcements about the Instagram Platform. You will also find tutorials and best practices to help you build great platform integrations. Make sure to subscribe to the RSS feed not to miss out on new posts: [http://developers.instagram.com](http://developers.instagram.com). 33 | 34 | 35 | Community 36 | ---------------------- 37 | The [Stack Overflow community](http://stackoverflow.com/questions/tagged/instagram/) is a great place to ask API related questions or if you need help with your code. Make sure to tag your questions with the Instagram tag to get fast answers from other fellow developers and members of the Instagram team. 38 | 39 | 40 | Authentication 41 | ----- 42 | 43 | Instagram API uses the OAuth2 protocol for authentication, but not all functionality requires authentication. 44 | See the docs for more information: http://instagram.com/developer/authentication/ 45 | 46 | ### Obtaining an access token 47 | 48 | If you're using a method that requires authentication and need an access token, you can use the provided get_access_token.py script to obtain an access token for yourself. 49 | It will prompt you for your app's Client ID, Client Secret, and Redirect URI, and walk you through instructions for getting your own access token for your app. 50 | 51 | ### Authenticating a user 52 | 53 | The provided sample app shows a simple OAuth flow for authenticating a user and getting an access token for them. 54 | 55 | ### Using an access token 56 | 57 | Once you have an access token (whether via the script or from the user flow), you can pass that token into the InstagramAPI constructor: 58 | 59 | ``` python 60 | from instagram.client import InstagramAPI 61 | 62 | access_token = "YOUR_ACCESS_TOKEN" 63 | client_secret = "YOUR_CLIENT_SECRET" 64 | api = InstagramAPI(access_token=access_token, client_secret=client_secret) 65 | recent_media, next_ = api.user_recent_media(user_id="userid", count=10) 66 | for media in recent_media: 67 | print media.caption.text 68 | ``` 69 | 70 | ### Making unauthenticated requests 71 | 72 | For methods that don't require authentication, you can just pass your client ID and optionally client secret into the InstagramAPI 73 | constructor: 74 | 75 | ``` python 76 | api = InstagramAPI(client_id='YOUR_CLIENT_ID', client_secret='YOUR_CLIENT_SECRET') 77 | popular_media = api.media_popular(count=20) 78 | for media in popular_media: 79 | print media.images['standard_resolution'].url 80 | ``` 81 | 82 | Real-time Subscriptions: 83 | ----- 84 | 85 | See the docs for more on real-time subscriptions: http://instagr.am/developer/realtime/ 86 | 87 | You can use the API to subscribe to users, tags, locations, or geographies: 88 | 89 | ``` python 90 | # Subscribe to updates for all users authenticated to your app 91 | api.create_subscription(object='user', aspect='media', callback_url='http://mydomain.com/hook/instagram') 92 | 93 | # Subscribe to all media tagged with 'fox' 94 | api.create_subscription(object='tag', object_id='fox', aspect='media', callback_url='http://mydomain.com/hook/instagram') 95 | 96 | # Subscribe to all media in a given location 97 | api.create_subscription(object='location', object_id='1257285', aspect='media', callback_url='http://mydomain.com/hook/instagram') 98 | 99 | # Subscribe to all media in a geographic area 100 | api.create_subscription(object='geography', lat=35.657872, lng=139.70232, radius=1000, aspect='media', callback_url='http://mydomain.com/hook/instagram') 101 | ``` 102 | 103 | Along with that, you would typically register subscription "reactors" for processing the different subscription types: 104 | 105 | ``` python 106 | # React to user type updates 107 | reactor = subscriptions.SubscriptionsReactor() 108 | reactor.register_callback(subscriptions.SubscriptionType.USER, process_user_update) 109 | ``` 110 | 111 | See the provided sample app for an example of making a subscription, reacting to it, an processing the updates. 112 | 113 | You can also use the API to list and delete subscriptions: 114 | 115 | ``` python 116 | api.list_subscriptions() 117 | api.delete_subscriptions(id=342342) 118 | ``` 119 | 120 | 121 | Data Retrieval: 122 | ----- 123 | 124 | See the endpoints docs for more on these methods: http://instagr.am/developer/endpoints/ 125 | 126 | The methods with a * return two values, where the second is a pagination parameter. Here's an example of retrieving recent media: 127 | 128 | ``` python 129 | recent_media, next_ = api.user_recent_media() 130 | photos = [] 131 | for media in recent_media: 132 | photos.append('' % media.images['thumbnail'].url) 133 | ``` 134 | 135 | And an example of exhaustively pursuing a paginated endpoint: 136 | 137 | ``` python 138 | follows, next_ = api.user_follows() 139 | while next_: 140 | more_follows, next_ = api.user_follows(with_next_url=next_) 141 | follows.extend(more_follows) 142 | ``` 143 | 144 | Users: http://instagr.am/developer/endpoints/users/ 145 | 146 | ``` python 147 | api.user(user_id) 148 | api.user_media_feed()* 149 | api.user_liked_media()* 150 | api.user_recent_media(user_id, count, max_id)* 151 | api.user_search(q, count, lat, lng, min_timestamp, max_timestamp) 152 | ``` 153 | 154 | Relationships: http://instagr.am/developer/endpoints/relationships/ 155 | 156 | ``` python 157 | api.user_incoming_requests() 158 | api.user_follows(user_id)* 159 | api.user_followed_by(user_id)* 160 | api.follow_user(user_id) 161 | api.unfollow_user(user_id) 162 | api.block_user(user_id) 163 | api.unblock_user(user_id) 164 | api.approve_user_request(user_id) 165 | api.ignore_user_request(user_id) 166 | api.user_relationship(user_id) 167 | ``` 168 | 169 | Media: http://instagr.am/developer/endpoints/media/ 170 | 171 | ``` python 172 | api.media(media_id) 173 | api.media_popular(count, max_id) 174 | api.media_search(q, count, lat, lng, min_timestamp, max_timestamp) 175 | ``` 176 | 177 | Comments: http://instagr.am/developer/endpoints/comments/ 178 | 179 | ``` python 180 | api.media_comments(media_id) 181 | api.create_media_comment(media_id, text) 182 | api.delete_comment(media_id, comment_id) 183 | ``` 184 | 185 | Likes: http://instagr.am/developer/endpoints/likes/ 186 | 187 | ``` python 188 | api.media_likes(media_id) 189 | api.like_media(media_id) 190 | api.unlike_media(media_id) 191 | ``` 192 | 193 | Tags: http://instagr.am/developer/endpoints/tags/ 194 | 195 | ``` python 196 | api.tag(tag_name) 197 | api.tag_recent_media(count, max_tag_id, tag_name)* 198 | api.tag_search(q, count)* 199 | ``` 200 | 201 | Locations: http://instagr.am/developer/endpoints/locations/ 202 | 203 | ``` python 204 | api.location(location_id) 205 | api.location_recent_media(count, max_id, location_id)* 206 | api.location_search(q, count, lat, lng, foursquare_id, foursquare_v2_id) 207 | ``` 208 | 209 | Geographies: http://instagr.am/developer/endpoints/geographies/ 210 | 211 | ``` python 212 | api.geography_recent_media(count, max_id, geography_id)* 213 | ``` 214 | 215 | Error handling 216 | ------ 217 | Importing the bind module allows handling of specific error status codes. An example is provided below: 218 | ``` python 219 | from instagram.bind import InstagramAPIError 220 | 221 | try: 222 | # your code goes here 223 | except InstagramAPIError as e: 224 | if (e.status_code == 400): 225 | print "\nUser is set to private." 226 | ``` 227 | 228 | Trouble Shooting 229 | ------ 230 | 231 | If you get an error of a module not being defined during the Instagram import call, this might update a necessary package. 232 | ``` 233 | sudo pip install --upgrade six 234 | ``` 235 | 236 | Sample app 237 | ------ 238 | This repository includes a one-file sample app that uses the bottle framework and demonstrates 239 | authentication, subscriptions, and update processing. To try it out: 240 | 241 | * Download bottle if you don't already have it: pip install bottle 242 | * Download bottle-session if you don't already have it: pip install bottle-session 243 | * Download and run a redis instance on port 6379 if you don't already have it. Check http://redis.io for instructions. 244 | * Set your redirect URI to 'http://localhost:8515/oauth_callback' in your dev profile 245 | * Open up sample\_app.py, update it with your client\_id and secret, and set redirect URI to 'http://localhost:8515/oauth_callback' 246 | * Run the file; it will host a local server on port 8515. 247 | * Try visiting http://localhost:8515 in your browser 248 | 249 | Contributing 250 | ------------ 251 | In the spirit of [free software](http://www.fsf.org/licensing/essays/free-sw.html), **everyone** is encouraged to help improve this project. 252 | 253 | Here are some ways *you* can contribute: 254 | 255 | * by using alpha, beta, and prerelease versions 256 | * by reporting bugs 257 | * by suggesting new features 258 | * by writing or editing documentation 259 | * by writing specifications 260 | * by writing code (**no patch is too small**: fix typos, add comments, clean up inconsistent whitespace) 261 | * by refactoring code 262 | * by closing [issues](http://github.com/Instagram/python-instagram/issues) 263 | * by reviewing patches 264 | 265 | 266 | Submitting an Issue 267 | ------------------- 268 | We use the [GitHub issue tracker](https://github.com/Instagram/python-instagram/issues) to track bugs and 269 | features. Before submitting a bug report or feature request, check to make sure it hasn't already 270 | been submitted. You can indicate support for an existing issue by voting it up. When submitting a 271 | bug report, please include a [Gist](http://gist.github.com/) that includes a stack trace and any 272 | details that may be necessary to reproduce the bug, including your version number, and 273 | operating system. Ideally, a bug report should include a pull request with failing specs. 274 | 275 | 276 | Submitting a Pull Request 277 | ------------------------- 278 | 1. Fork the project. 279 | 2. Create a topic branch. 280 | 3. Implement your feature or bug fix. 281 | 4. Run python tests.py . 282 | 5. Add a test for your feature or bug fix. 283 | 6. Run python tests.py . If your changes are not 100% covered, go back to step 5. 284 | 7. Commit and push your changes. 285 | 8. Submit a pull request. 286 | 9. If you haven't already, complete the Contributor License Agreement ("CLA"). 287 | 288 | Contributor License Agreement ("CLA") 289 | _____________________________________ 290 | In order to accept your pull request, we need you to submit a CLA. You only need 291 | to do this once to work on any of Instagram's or Facebook's open source projects. 292 | 293 | Complete your CLA here: [https://code.facebook.com/cla](https://code.facebook.com/cla) 294 | 295 | 296 | Copyright 297 | --------- 298 | Copyright (c) 2014, Facebook, Inc. All rights reserved. 299 | By contributing to python-instagram, you agree that your contributions will be licensed under its BSD license. 300 | See [LICENSE](https://github.com/Instagram/python-instagram/blob/master/LICENSE.md) for details. 301 | -------------------------------------------------------------------------------- /sample_app.py: -------------------------------------------------------------------------------- 1 | import bottle 2 | import beaker.middleware 3 | from bottle import route, redirect, post, run, request, hook 4 | from instagram import client, subscriptions 5 | 6 | bottle.debug(True) 7 | 8 | session_opts = { 9 | 'session.type': 'file', 10 | 'session.data_dir': './session/', 11 | 'session.auto': True, 12 | } 13 | 14 | app = beaker.middleware.SessionMiddleware(bottle.app(), session_opts) 15 | 16 | CONFIG = { 17 | 'client_id': '', 18 | 'client_secret': '', 19 | 'redirect_uri': 'http://localhost:8515/oauth_callback' 20 | } 21 | 22 | unauthenticated_api = client.InstagramAPI(**CONFIG) 23 | 24 | @hook('before_request') 25 | def setup_request(): 26 | request.session = request.environ['beaker.session'] 27 | 28 | def process_tag_update(update): 29 | print(update) 30 | 31 | reactor = subscriptions.SubscriptionsReactor() 32 | reactor.register_callback(subscriptions.SubscriptionType.TAG, process_tag_update) 33 | 34 | @route('/') 35 | def home(): 36 | try: 37 | url = unauthenticated_api.get_authorize_url(scope=["likes","comments"]) 38 | return 'Connect with Instagram' % url 39 | except Exception as e: 40 | print(e) 41 | 42 | def get_nav(): 43 | nav_menu = ("

Python Instagram

" 44 | "") 55 | return nav_menu 56 | 57 | @route('/oauth_callback') 58 | def on_callback(): 59 | code = request.GET.get("code") 60 | if not code: 61 | return 'Missing code' 62 | try: 63 | access_token, user_info = unauthenticated_api.exchange_code_for_access_token(code) 64 | if not access_token: 65 | return 'Could not get access token' 66 | api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) 67 | request.session['access_token'] = access_token 68 | except Exception as e: 69 | print(e) 70 | return get_nav() 71 | 72 | @route('/recent') 73 | def on_recent(): 74 | content = "

User Recent Media

" 75 | access_token = request.session['access_token'] 76 | if not access_token: 77 | return 'Missing Access Token' 78 | try: 79 | api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) 80 | recent_media, next = api.user_recent_media() 81 | photos = [] 82 | for media in recent_media: 83 | photos.append('
') 84 | if(media.type == 'video'): 85 | photos.append('' % (media.get_standard_resolution_url())) 86 | else: 87 | photos.append('' % (media.get_low_resolution_url())) 88 | photos.append("
Like Un-Like LikesCount=%s
" % (media.id,media.id,media.like_count)) 89 | content += ''.join(photos) 90 | except Exception as e: 91 | print(e) 92 | return "%s %s
Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) 93 | 94 | @route('/media_like/') 95 | def media_like(id): 96 | access_token = request.session['access_token'] 97 | api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) 98 | api.like_media(media_id=id) 99 | redirect("/recent") 100 | 101 | @route('/media_unlike/') 102 | def media_unlike(id): 103 | access_token = request.session['access_token'] 104 | api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) 105 | api.unlike_media(media_id=id) 106 | redirect("/recent") 107 | 108 | @route('/user_media_feed') 109 | def on_user_media_feed(): 110 | access_token = request.session['access_token'] 111 | content = "

User Media Feed

" 112 | if not access_token: 113 | return 'Missing Access Token' 114 | try: 115 | api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) 116 | media_feed, next = api.user_media_feed() 117 | photos = [] 118 | for media in media_feed: 119 | photos.append('' % media.get_standard_resolution_url()) 120 | counter = 1 121 | while next and counter < 3: 122 | media_feed, next = api.user_media_feed(with_next_url=next) 123 | for media in media_feed: 124 | photos.append('' % media.get_standard_resolution_url()) 125 | counter += 1 126 | content += ''.join(photos) 127 | except Exception as e: 128 | print(e) 129 | return "%s %s
Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) 130 | 131 | @route('/location_recent_media') 132 | def location_recent_media(): 133 | access_token = request.session['access_token'] 134 | content = "

Location Recent Media

" 135 | if not access_token: 136 | return 'Missing Access Token' 137 | try: 138 | api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) 139 | recent_media, next = api.location_recent_media(location_id=514276) 140 | photos = [] 141 | for media in recent_media: 142 | photos.append('' % media.get_standard_resolution_url()) 143 | content += ''.join(photos) 144 | except Exception as e: 145 | print(e) 146 | return "%s %s
Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) 147 | 148 | @route('/media_search') 149 | def media_search(): 150 | access_token = request.session['access_token'] 151 | content = "

Media Search

" 152 | if not access_token: 153 | return 'Missing Access Token' 154 | try: 155 | api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) 156 | media_search = api.media_search(lat="37.7808851",lng="-122.3948632",distance=1000) 157 | photos = [] 158 | for media in media_search: 159 | photos.append('' % media.get_standard_resolution_url()) 160 | content += ''.join(photos) 161 | except Exception as e: 162 | print(e) 163 | return "%s %s
Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) 164 | 165 | @route('/media_popular') 166 | def media_popular(): 167 | access_token = request.session['access_token'] 168 | content = "

Popular Media

" 169 | if not access_token: 170 | return 'Missing Access Token' 171 | try: 172 | api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) 173 | media_search = api.media_popular() 174 | photos = [] 175 | for media in media_search: 176 | photos.append('' % media.get_standard_resolution_url()) 177 | content += ''.join(photos) 178 | except Exception as e: 179 | print(e) 180 | return "%s %s
Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) 181 | 182 | @route('/user_search') 183 | def user_search(): 184 | access_token = request.session['access_token'] 185 | content = "

User Search

" 186 | if not access_token: 187 | return 'Missing Access Token' 188 | try: 189 | api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) 190 | user_search = api.user_search(q="Instagram") 191 | users = [] 192 | for user in user_search: 193 | users.append('
  • %s
  • ' % (user.profile_picture,user.username)) 194 | content += ''.join(users) 195 | except Exception as e: 196 | print(e) 197 | return "%s %s
    Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) 198 | 199 | @route('/user_follows') 200 | def user_follows(): 201 | access_token = request.session['access_token'] 202 | content = "

    User Follows

    " 203 | if not access_token: 204 | return 'Missing Access Token' 205 | try: 206 | api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) 207 | # 25025320 is http://instagram.com/instagram 208 | user_follows, next = api.user_follows('25025320') 209 | users = [] 210 | for user in user_follows: 211 | users.append('
  • %s
  • ' % (user.profile_picture,user.username)) 212 | while next: 213 | user_follows, next = api.user_follows(with_next_url=next) 214 | for user in user_follows: 215 | users.append('
  • %s
  • ' % (user.profile_picture,user.username)) 216 | content += ''.join(users) 217 | except Exception as e: 218 | print(e) 219 | return "%s %s
    Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) 220 | 221 | @route('/location_search') 222 | def location_search(): 223 | access_token = request.session['access_token'] 224 | content = "

    Location Search

    " 225 | if not access_token: 226 | return 'Missing Access Token' 227 | try: 228 | api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) 229 | location_search = api.location_search(lat="37.7808851",lng="-122.3948632",distance=1000) 230 | locations = [] 231 | for location in location_search: 232 | locations.append('
  • %s Map
  • ' % (location.name,location.point.latitude,location.point.longitude)) 233 | content += ''.join(locations) 234 | except Exception as e: 235 | print(e) 236 | return "%s %s
    Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) 237 | 238 | @route('/tag_search') 239 | def tag_search(): 240 | access_token = request.session['access_token'] 241 | content = "

    Tag Search

    " 242 | if not access_token: 243 | return 'Missing Access Token' 244 | try: 245 | api = client.InstagramAPI(access_token=access_token, client_secret=CONFIG['client_secret']) 246 | tag_search, next_tag = api.tag_search(q="backclimateaction") 247 | tag_recent_media, next = api.tag_recent_media(tag_name=tag_search[0].name) 248 | photos = [] 249 | for tag_media in tag_recent_media: 250 | photos.append('' % tag_media.get_standard_resolution_url()) 251 | content += ''.join(photos) 252 | except Exception as e: 253 | print(e) 254 | return "%s %s
    Remaining API Calls = %s/%s" % (get_nav(),content,api.x_ratelimit_remaining,api.x_ratelimit) 255 | 256 | @route('/realtime_callback') 257 | @post('/realtime_callback') 258 | def on_realtime_callback(): 259 | mode = request.GET.get("hub.mode") 260 | challenge = request.GET.get("hub.challenge") 261 | verify_token = request.GET.get("hub.verify_token") 262 | if challenge: 263 | return challenge 264 | else: 265 | x_hub_signature = request.header.get('X-Hub-Signature') 266 | raw_response = request.body.read() 267 | try: 268 | reactor.process(CONFIG['client_secret'], raw_response, x_hub_signature) 269 | except subscriptions.SubscriptionVerifyError: 270 | print("Signature mismatch") 271 | 272 | bottle.run(app=app, host='localhost', port=8515, reloader=True) 273 | -------------------------------------------------------------------------------- /fixtures/media_search.json: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "code": 200 4 | }, 5 | "data": [ 6 | { 7 | "type": "image", 8 | "tags": [], 9 | "location": { 10 | "latitude": 37.775382999999991, 11 | "id": "74480", 12 | "longitude": -122.223941, 13 | "name": "Fruitvale BART" 14 | }, 15 | "comments": { 16 | "count": 1, 17 | "data": [ 18 | { 19 | "created_time": "1288931764", 20 | "text": "Emergency", 21 | "from": { 22 | "username": "catchfoot", 23 | "first_name": "Rachel", 24 | "last_name": "Lightfoot", 25 | "type": "user", 26 | "id": "208329" 27 | }, 28 | "id": "1916879" 29 | } 30 | ] 31 | }, 32 | "caption": { 33 | "created_time": "1288931764", 34 | "text": "Emergency", 35 | "from": { 36 | "username": "catchfoot", 37 | "first_name": "Rachel", 38 | "last_name": "Lightfoot", 39 | "type": "user", 40 | "id": "208329" 41 | }, 42 | "id": "1916879" 43 | }, 44 | "link": "http://localhost:8000/p/J2aL/", 45 | "likes": { 46 | "count": 0 47 | }, 48 | "created_time": "1288931757", 49 | "images": { 50 | "low_resolution": { 51 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/11/04/486c002f2fd14e2f8a1aef99dee7b95d_6.jpg", 52 | "width": 480, 53 | "height": 480 54 | }, 55 | "thumbnail": { 56 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/11/04/486c002f2fd14e2f8a1aef99dee7b95d_5.jpg", 57 | "width": 150, 58 | "height": 150 59 | }, 60 | "standard_resolution": { 61 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/11/04/486c002f2fd14e2f8a1aef99dee7b95d_7.jpg", 62 | "width": 612, 63 | "height": 612 64 | } 65 | }, 66 | "user_has_liked": false, 67 | "id": "2582155", 68 | "user": { 69 | "username": "catchfoot", 70 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_208329_75sq_1288164413.jpg", 71 | "id": "208329" 72 | } 73 | }, 74 | { 75 | "type": "image", 76 | "tags": [], 77 | "location": { 78 | "latitude": 37.775289999999998, 79 | "id": "163042", 80 | "longitude": -122.224172, 81 | "name": "Powderface" 82 | }, 83 | "comments": { 84 | "count": 1, 85 | "data": [ 86 | { 87 | "created_time": "1288644742", 88 | "text": "Caramel Beignets! Yummy :)", 89 | "from": { 90 | "username": "mkram0s", 91 | "first_name": "Kate", 92 | "last_name": "Ramos", 93 | "type": "user", 94 | "id": "305504" 95 | }, 96 | "id": "1494733" 97 | } 98 | ] 99 | }, 100 | "caption": { 101 | "created_time": "1288644742", 102 | "text": "Caramel Beignets! Yummy :)", 103 | "from": { 104 | "username": "mkram0s", 105 | "first_name": "Kate", 106 | "last_name": "Ramos", 107 | "type": "user", 108 | "id": "305504" 109 | }, 110 | "id": "1494733" 111 | }, 112 | "link": "http://localhost:8000/p/H9R_/", 113 | "likes": { 114 | "count": 0 115 | }, 116 | "created_time": "1288644633", 117 | "images": { 118 | "low_resolution": { 119 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/11/01/7a74951a954a44619b5b2afb64a7b17d_6.jpg", 120 | "width": 480, 121 | "height": 480 122 | }, 123 | "thumbnail": { 124 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/11/01/7a74951a954a44619b5b2afb64a7b17d_5.jpg", 125 | "width": 150, 126 | "height": 150 127 | }, 128 | "standard_resolution": { 129 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/11/01/7a74951a954a44619b5b2afb64a7b17d_7.jpg", 130 | "width": 612, 131 | "height": 612 132 | } 133 | }, 134 | "user_has_liked": false, 135 | "id": "2086015", 136 | "user": { 137 | "username": "mkram0s", 138 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_305504_75sq_1288204024.jpg", 139 | "id": "305504" 140 | } 141 | }, 142 | { 143 | "type": "image", 144 | "tags": [], 145 | "location": { 146 | "latitude": 37.775382999999991, 147 | "id": "74480", 148 | "longitude": -122.223941, 149 | "name": "Fruitvale BART" 150 | }, 151 | "comments": { 152 | "count": 1, 153 | "data": [ 154 | { 155 | "created_time": "1288232768", 156 | "text": "Bored on Bart ", 157 | "from": { 158 | "username": "hannahc", 159 | "first_name": "Hannah", 160 | "last_name": "Coughlin ", 161 | "type": "user", 162 | "id": "304020" 163 | }, 164 | "id": "1030446" 165 | } 166 | ] 167 | }, 168 | "caption": { 169 | "created_time": "1288232768", 170 | "text": "Bored on Bart ", 171 | "from": { 172 | "username": "hannahc", 173 | "first_name": "Hannah", 174 | "last_name": "Coughlin ", 175 | "type": "user", 176 | "id": "304020" 177 | }, 178 | "id": "1030446" 179 | }, 180 | "link": "http://localhost:8000/p/Fm-m/", 181 | "likes": { 182 | "count": 0 183 | }, 184 | "created_time": "1288232753", 185 | "images": { 186 | "low_resolution": { 187 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/27/1406596f54be46b4a538dd1d4b4c6f1e_6.jpg", 188 | "width": 480, 189 | "height": 480 190 | }, 191 | "thumbnail": { 192 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/27/1406596f54be46b4a538dd1d4b4c6f1e_5.jpg", 193 | "width": 150, 194 | "height": 150 195 | }, 196 | "standard_resolution": { 197 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/27/1406596f54be46b4a538dd1d4b4c6f1e_7.jpg", 198 | "width": 612, 199 | "height": 612 200 | } 201 | }, 202 | "user_has_liked": false, 203 | "id": "1470374", 204 | "user": { 205 | "username": "hannahc", 206 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_304020_75sq_1288194960.jpg", 207 | "id": "304020" 208 | } 209 | }, 210 | { 211 | "type": "image", 212 | "tags": [], 213 | "location": { 214 | "latitude": 37.775382999999991, 215 | "id": "74480", 216 | "longitude": -122.223941, 217 | "name": "Fruitvale BART" 218 | }, 219 | "comments": { 220 | "count": 1, 221 | "data": [ 222 | { 223 | "created_time": "1288136425", 224 | "text": "Emergency", 225 | "from": { 226 | "username": "rolliefingaz", 227 | "first_name": "Gregory", 228 | "last_name": "Hurcomb", 229 | "type": "user", 230 | "id": "233618" 231 | }, 232 | "id": "951869" 233 | } 234 | ] 235 | }, 236 | "caption": { 237 | "created_time": "1288136425", 238 | "text": "Emergency", 239 | "from": { 240 | "username": "rolliefingaz", 241 | "first_name": "Gregory", 242 | "last_name": "Hurcomb", 243 | "type": "user", 244 | "id": "233618" 245 | }, 246 | "id": "951869" 247 | }, 248 | "link": "http://localhost:8000/p/FOZf/", 249 | "likes": { 250 | "count": 0 251 | }, 252 | "created_time": "1288136246", 253 | "images": { 254 | "low_resolution": { 255 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/26/3459f1db2d6d422b950838715a04cb38_6.jpg", 256 | "width": 480, 257 | "height": 480 258 | }, 259 | "thumbnail": { 260 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/26/3459f1db2d6d422b950838715a04cb38_5.jpg", 261 | "width": 150, 262 | "height": 150 263 | }, 264 | "standard_resolution": { 265 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/26/3459f1db2d6d422b950838715a04cb38_7.jpg", 266 | "width": 612, 267 | "height": 612 268 | } 269 | }, 270 | "user_has_liked": false, 271 | "id": "1369695", 272 | "user": { 273 | "username": "rolliefingaz", 274 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_233618_75sq_1287785819.jpg", 275 | "id": "233618" 276 | } 277 | }, 278 | { 279 | "type": "image", 280 | "tags": [], 281 | "location": { 282 | "latitude": 37.775194499999998, 283 | "id": "101132", 284 | "longitude": -122.227087, 285 | "name": "Guadalajara Mexican Cuisine and Bar" 286 | }, 287 | "comments": { 288 | "count": 2, 289 | "data": [ 290 | { 291 | "created_time": "1287969851", 292 | "text": "Well hello there beautiful", 293 | "from": { 294 | "username": "jorstaff", 295 | "first_name": "Jordan", 296 | "last_name": "Walker", 297 | "type": "user", 298 | "id": "154425" 299 | }, 300 | "id": "818311" 301 | }, 302 | { 303 | "created_time": "1288072257", 304 | "text": "Text me next time. I live down the st and I'm ways down for tacos!!", 305 | "from": { 306 | "username": "melisaarreguin", 307 | "first_name": "Melisa", 308 | "last_name": "Arreguin", 309 | "type": "user", 310 | "id": "122725" 311 | }, 312 | "id": "898954" 313 | } 314 | ] 315 | }, 316 | "caption": { 317 | "created_time": "1287969851", 318 | "text": "Well hello there beautiful", 319 | "from": { 320 | "username": "jorstaff", 321 | "first_name": "Jordan", 322 | "last_name": "Walker", 323 | "type": "user", 324 | "id": "154425" 325 | }, 326 | "id": "818311" 327 | }, 328 | "link": "http://localhost:8000/p/Ejh5/", 329 | "likes": { 330 | "count": 1 331 | }, 332 | "created_time": "1287969827", 333 | "images": { 334 | "low_resolution": { 335 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/24/8a466d809fe442a1a2df024543ae25e8_6.jpg", 336 | "width": 480, 337 | "height": 480 338 | }, 339 | "thumbnail": { 340 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/24/8a466d809fe442a1a2df024543ae25e8_5.jpg", 341 | "width": 150, 342 | "height": 150 343 | }, 344 | "standard_resolution": { 345 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/24/8a466d809fe442a1a2df024543ae25e8_7.jpg", 346 | "width": 612, 347 | "height": 612 348 | } 349 | }, 350 | "user_has_liked": false, 351 | "id": "1194105", 352 | "user": { 353 | "username": "jorstaff", 354 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_154425_75sq_1287413622.jpg", 355 | "id": "154425" 356 | } 357 | }, 358 | { 359 | "type": "image", 360 | "tags": [], 361 | "location": { 362 | "latitude": 37.775382999999991, 363 | "id": "74480", 364 | "longitude": -122.223941, 365 | "name": "Fruitvale BART" 366 | }, 367 | "comments": { 368 | "count": 1, 369 | "data": [ 370 | { 371 | "created_time": "1287674437", 372 | "text": "Morning commute", 373 | "from": { 374 | "username": "catchfoot", 375 | "first_name": "Rachel", 376 | "last_name": "Lightfoot", 377 | "type": "user", 378 | "id": "208329" 379 | }, 380 | "id": "555932" 381 | } 382 | ] 383 | }, 384 | "caption": { 385 | "created_time": "1287674437", 386 | "text": "Morning commute", 387 | "from": { 388 | "username": "catchfoot", 389 | "first_name": "Rachel", 390 | "last_name": "Lightfoot", 391 | "type": "user", 392 | "id": "208329" 393 | }, 394 | "id": "555932" 395 | }, 396 | "link": "http://localhost:8000/p/DMl1/", 397 | "likes": { 398 | "count": 0 399 | }, 400 | "created_time": "1287674360", 401 | "images": { 402 | "low_resolution": { 403 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/21/d9949ca81cb540d798acee59ea94223a_6.jpg", 404 | "width": 480, 405 | "height": 480 406 | }, 407 | "thumbnail": { 408 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/21/d9949ca81cb540d798acee59ea94223a_5.jpg", 409 | "width": 150, 410 | "height": 150 411 | }, 412 | "standard_resolution": { 413 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/21/d9949ca81cb540d798acee59ea94223a_7.jpg", 414 | "width": 612, 415 | "height": 612 416 | } 417 | }, 418 | "user_has_liked": false, 419 | "id": "838005", 420 | "user": { 421 | "username": "catchfoot", 422 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_208329_75sq_1288164413.jpg", 423 | "id": "208329" 424 | } 425 | }, 426 | { 427 | "type": "image", 428 | "tags": [], 429 | "location": { 430 | "latitude": 37.775180799999987, 431 | "id": "68841", 432 | "longitude": -122.2270716, 433 | "name": "El Novillo Taco Truck" 434 | }, 435 | "comments": { 436 | "count": 4, 437 | "data": [ 438 | { 439 | "created_time": "1287585720", 440 | "text": "Carne Asasa Torta. No Onions. ", 441 | "from": { 442 | "username": "darodriguez", 443 | "first_name": "David", 444 | "last_name": "Rodriguez", 445 | "type": "user", 446 | "id": "113603" 447 | }, 448 | "id": "495555" 449 | }, 450 | { 451 | "created_time": "1287585854", 452 | "text": "Hey, I wanted onions. :(", 453 | "from": { 454 | "username": "melisaarreguin", 455 | "first_name": "Melisa", 456 | "last_name": "Arreguin", 457 | "type": "user", 458 | "id": "122725" 459 | }, 460 | "id": "495675" 461 | }, 462 | { 463 | "created_time": "1287603871", 464 | "text": "You got onions. You had the al pastor, remember!?", 465 | "from": { 466 | "username": "darodriguez", 467 | "first_name": "David", 468 | "last_name": "Rodriguez", 469 | "type": "user", 470 | "id": "113603" 471 | }, 472 | "id": "507172" 473 | }, 474 | { 475 | "created_time": "1287606841", 476 | "text": "Oh shiiiit this would have taken everything to the next level", 477 | "from": { 478 | "username": "jorstaff", 479 | "first_name": "Jordan", 480 | "last_name": "Walker", 481 | "type": "user", 482 | "id": "154425" 483 | }, 484 | "id": "508433" 485 | } 486 | ] 487 | }, 488 | "caption": { 489 | "created_time": "1287585720", 490 | "text": "Carne Asasa Torta. No Onions. ", 491 | "from": { 492 | "username": "darodriguez", 493 | "first_name": "David", 494 | "last_name": "Rodriguez", 495 | "type": "user", 496 | "id": "113603" 497 | }, 498 | "id": "495555" 499 | }, 500 | "link": "http://localhost:8000/p/C5cU/", 501 | "likes": { 502 | "count": 1 503 | }, 504 | "created_time": "1287585671", 505 | "images": { 506 | "low_resolution": { 507 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/20/ad6e8429c40c40ffabe0813ba302a479_6.jpg", 508 | "width": 480, 509 | "height": 480 510 | }, 511 | "thumbnail": { 512 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/20/ad6e8429c40c40ffabe0813ba302a479_5.jpg", 513 | "width": 150, 514 | "height": 150 515 | }, 516 | "standard_resolution": { 517 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/20/ad6e8429c40c40ffabe0813ba302a479_7.jpg", 518 | "width": 612, 519 | "height": 612 520 | } 521 | }, 522 | "user_has_liked": false, 523 | "id": "759572", 524 | "user": { 525 | "username": "darodriguez", 526 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_113603_75sq_1287035206.jpg", 527 | "id": "113603" 528 | } 529 | }, 530 | { 531 | "type": "image", 532 | "tags": [], 533 | "location": { 534 | "latitude": 37.775180799999987, 535 | "id": "68841", 536 | "longitude": -122.2270716, 537 | "name": "El Novillo Taco Truck" 538 | }, 539 | "comments": { 540 | "count": 1, 541 | "data": [ 542 | { 543 | "created_time": "1287585453", 544 | "text": "Tortas. ", 545 | "from": { 546 | "username": "darodriguez", 547 | "first_name": "David", 548 | "last_name": "Rodriguez", 549 | "type": "user", 550 | "id": "113603" 551 | }, 552 | "id": "495311" 553 | } 554 | ] 555 | }, 556 | "caption": { 557 | "created_time": "1287585453", 558 | "text": "Tortas. ", 559 | "from": { 560 | "username": "darodriguez", 561 | "first_name": "David", 562 | "last_name": "Rodriguez", 563 | "type": "user", 564 | "id": "113603" 565 | }, 566 | "id": "495311" 567 | }, 568 | "link": "http://localhost:8000/p/C5Wr/", 569 | "likes": { 570 | "count": 0 571 | }, 572 | "created_time": "1287585407", 573 | "images": { 574 | "low_resolution": { 575 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/20/1fde15405b6349ef949c9a8f5498868e_6.jpg", 576 | "width": 480, 577 | "height": 480 578 | }, 579 | "thumbnail": { 580 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/20/1fde15405b6349ef949c9a8f5498868e_5.jpg", 581 | "width": 150, 582 | "height": 150 583 | }, 584 | "standard_resolution": { 585 | "url": "http://distillery-dev.s3.amazonaws.com/media/2010/10/20/1fde15405b6349ef949c9a8f5498868e_7.jpg", 586 | "width": 612, 587 | "height": 612 588 | } 589 | }, 590 | "user_has_liked": false, 591 | "id": "759211", 592 | "user": { 593 | "username": "darodriguez", 594 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_113603_75sq_1287035206.jpg", 595 | "id": "113603" 596 | } 597 | } 598 | ] 599 | } -------------------------------------------------------------------------------- /fixtures/geography_recent_media.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "caption": { 5 | "created_time": "1298688893", 6 | "from": { 7 | "full_name": "\ue32eLuzmila\ue32e", 8 | "id": "1056886", 9 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1056886_75sq_1297088163.jpg", 10 | "username": "lulu_love17" 11 | }, 12 | "id": "38108230", 13 | "text": "Ocean Drive \ue43e \ue32a" 14 | }, 15 | "comments": { 16 | "count": 0, 17 | "data": [] 18 | }, 19 | "created_time": "1298688799", 20 | "filter": "Hefe", 21 | "id": "31000411", 22 | "images": { 23 | "low_resolution": { 24 | "height": 306, 25 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/ab5beb5454ea4c85b7fd90ed02bd6d0c_6.jpg", 26 | "width": 306 27 | }, 28 | "standard_resolution": { 29 | "height": 612, 30 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/ab5beb5454ea4c85b7fd90ed02bd6d0c_7.jpg", 31 | "width": 612 32 | }, 33 | "thumbnail": { 34 | "height": 150, 35 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/ab5beb5454ea4c85b7fd90ed02bd6d0c_5.jpg", 36 | "width": 150 37 | } 38 | }, 39 | "likes": { 40 | "count": 2, 41 | "data": [ 42 | { 43 | "full_name": "Withinmysol", 44 | "id": "1068656", 45 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1068656_75sq_1295187999.jpg", 46 | "username": "withinmysol" 47 | }, 48 | { 49 | "full_name": "", 50 | "id": "1397022", 51 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1397022_75sq_1296406549.jpg", 52 | "username": "erynn" 53 | } 54 | ] 55 | }, 56 | "link": "http://instagr.am/p/B2Qdb/", 57 | "location": { 58 | "id": "1313027", 59 | "latitude": 25.775409, 60 | "longitude": -80.134048000000007, 61 | "name": "South Beach, Fl" 62 | }, 63 | "tags": [], 64 | "type": "image", 65 | "user": { 66 | "full_name": "\ue32eLuzmila\ue32e", 67 | "id": "1056886", 68 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1056886_75sq_1297088163.jpg", 69 | "username": "lulu_love17" 70 | } 71 | }, 72 | { 73 | "caption": { 74 | "created_time": "1298674022", 75 | "from": { 76 | "full_name": "Kent Nguyen", 77 | "id": "33287", 78 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_33287_75sq_1287025465.jpg", 79 | "username": "kentnguyen" 80 | }, 81 | "id": "38006529", 82 | "text": "Gay." 83 | }, 84 | "comments": { 85 | "count": 0, 86 | "data": [] 87 | }, 88 | "created_time": "1298674017", 89 | "filter": "Inkwell", 90 | "id": "30927855", 91 | "images": { 92 | "low_resolution": { 93 | "height": 306, 94 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/2ada6e0b96074eaf8b6d7f939febeb58_6.jpg", 95 | "width": 306 96 | }, 97 | "standard_resolution": { 98 | "height": 612, 99 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/2ada6e0b96074eaf8b6d7f939febeb58_7.jpg", 100 | "width": 612 101 | }, 102 | "thumbnail": { 103 | "height": 150, 104 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/2ada6e0b96074eaf8b6d7f939febeb58_5.jpg", 105 | "width": 150 106 | } 107 | }, 108 | "likes": { 109 | "count": 0, 110 | "data": [] 111 | }, 112 | "link": "http://instagr.am/p/B1-vv/", 113 | "location": { 114 | "id": "671519", 115 | "latitude": 25.782869000000002, 116 | "longitude": -80.130409999999998, 117 | "name": "Palace Bar" 118 | }, 119 | "tags": [], 120 | "type": "image", 121 | "user": { 122 | "full_name": "Kent Nguyen", 123 | "id": "33287", 124 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_33287_75sq_1287025465.jpg", 125 | "username": "kentnguyen" 126 | } 127 | }, 128 | { 129 | "caption": { 130 | "created_time": "1298673435", 131 | "from": { 132 | "full_name": "Jerid Choen", 133 | "id": "2198895", 134 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_2198895_75sq_1298664722.jpg", 135 | "username": "fiveski" 136 | }, 137 | "id": "38002786", 138 | "text": "A whole lotta coffee " 139 | }, 140 | "comments": { 141 | "count": 1, 142 | "data": [ 143 | { 144 | "created_time": "1298674595", 145 | "from": { 146 | "full_name": "", 147 | "id": "790053", 148 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_790053_75sq_1290570046.jpg", 149 | "username": "tommycalvo" 150 | }, 151 | "id": "38009996", 152 | "text": "Wired just looking at this." 153 | } 154 | ] 155 | }, 156 | "created_time": "1298673399", 157 | "filter": "Lomo-fi", 158 | "id": "30925082", 159 | "images": { 160 | "low_resolution": { 161 | "height": 306, 162 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/80dac67f35f94eb79ee4d40654127b04_6.jpg", 163 | "width": 306 164 | }, 165 | "standard_resolution": { 166 | "height": 612, 167 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/80dac67f35f94eb79ee4d40654127b04_7.jpg", 168 | "width": 612 169 | }, 170 | "thumbnail": { 171 | "height": 150, 172 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/80dac67f35f94eb79ee4d40654127b04_5.jpg", 173 | "width": 150 174 | } 175 | }, 176 | "likes": { 177 | "count": 0, 178 | "data": [] 179 | }, 180 | "link": "http://instagr.am/p/B1-Ea/", 181 | "location": { 182 | "id": "402760", 183 | "latitude": 25.799709, 184 | "longitude": -80.127409999999998, 185 | "name": "Gansevoort Hotel" 186 | }, 187 | "tags": [], 188 | "type": "image", 189 | "user": { 190 | "full_name": "Jerid Choen", 191 | "id": "2198895", 192 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_2198895_75sq_1298664722.jpg", 193 | "username": "fiveski" 194 | } 195 | }, 196 | { 197 | "caption": { 198 | "created_time": "1298672337", 199 | "from": { 200 | "full_name": "Jerid Choen", 201 | "id": "2198895", 202 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_2198895_75sq_1298664722.jpg", 203 | "username": "fiveski" 204 | }, 205 | "id": "37996060", 206 | "text": "Hello! Kiddies!" 207 | }, 208 | "comments": { 209 | "count": 1, 210 | "data": [ 211 | { 212 | "created_time": "1298674541", 213 | "from": { 214 | "full_name": "", 215 | "id": "790053", 216 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_790053_75sq_1290570046.jpg", 217 | "username": "tommycalvo" 218 | }, 219 | "id": "38009644", 220 | "text": "Knots! Love the get up." 221 | } 222 | ] 223 | }, 224 | "created_time": "1298672333", 225 | "filter": "Apollo", 226 | "id": "30920336", 227 | "images": { 228 | "low_resolution": { 229 | "height": 306, 230 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/1db76b6bd85d45d9b5def24ab311f4cc_6.jpg", 231 | "width": 306 232 | }, 233 | "standard_resolution": { 234 | "height": 612, 235 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/1db76b6bd85d45d9b5def24ab311f4cc_7.jpg", 236 | "width": 612 237 | }, 238 | "thumbnail": { 239 | "height": 150, 240 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/1db76b6bd85d45d9b5def24ab311f4cc_5.jpg", 241 | "width": 150 242 | } 243 | }, 244 | "likes": { 245 | "count": 0, 246 | "data": [] 247 | }, 248 | "link": "http://instagr.am/p/B186Q/", 249 | "location": { 250 | "id": "81620", 251 | "latitude": 25.797829, 252 | "longitude": -80.127860999999996, 253 | "name": "W Hotel - South Beach" 254 | }, 255 | "tags": [], 256 | "type": "image", 257 | "user": { 258 | "full_name": "Jerid Choen", 259 | "id": "2198895", 260 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_2198895_75sq_1298664722.jpg", 261 | "username": "fiveski" 262 | } 263 | }, 264 | { 265 | "caption": { 266 | "created_time": "1298670942", 267 | "from": { 268 | "full_name": "Fabian Alcantara", 269 | "id": "16073", 270 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_16073_75sq_1289251767.jpg", 271 | "username": "fabuloso" 272 | }, 273 | "id": "37987914", 274 | "text": "Perfect blue skies" 275 | }, 276 | "comments": { 277 | "count": 0, 278 | "data": [] 279 | }, 280 | "created_time": "1298670941", 281 | "filter": "X-Pro II", 282 | "id": "30914462", 283 | "images": { 284 | "low_resolution": { 285 | "height": 306, 286 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/9838b83154a44c1f9e13366fbd97496e_6.jpg", 287 | "width": 306 288 | }, 289 | "standard_resolution": { 290 | "height": 612, 291 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/9838b83154a44c1f9e13366fbd97496e_7.jpg", 292 | "width": 612 293 | }, 294 | "thumbnail": { 295 | "height": 150, 296 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/9838b83154a44c1f9e13366fbd97496e_5.jpg", 297 | "width": 150 298 | } 299 | }, 300 | "likes": { 301 | "count": 0, 302 | "data": [] 303 | }, 304 | "link": "http://instagr.am/p/B17ee/", 305 | "location": { 306 | "id": "499461", 307 | "latitude": 25.783766, 308 | "longitude": -80.130262999999999, 309 | "name": "South Beach" 310 | }, 311 | "tags": [], 312 | "type": "image", 313 | "user": { 314 | "full_name": "Fabian Alcantara", 315 | "id": "16073", 316 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_16073_75sq_1289251767.jpg", 317 | "username": "fabuloso" 318 | } 319 | }, 320 | { 321 | "caption": { 322 | "created_time": "1298668492", 323 | "from": { 324 | "full_name": "Kent Nguyen", 325 | "id": "33287", 326 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_33287_75sq_1287025465.jpg", 327 | "username": "kentnguyen" 328 | }, 329 | "id": "37974294", 330 | "text": "The epitome of beach drinking" 331 | }, 332 | "comments": { 333 | "count": 0, 334 | "data": [] 335 | }, 336 | "created_time": "1298668487", 337 | "filter": "X-Pro II", 338 | "id": "30904041", 339 | "images": { 340 | "low_resolution": { 341 | "height": 306, 342 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/634e57abbbba46adbff9950574d8b02f_6.jpg", 343 | "width": 306 344 | }, 345 | "standard_resolution": { 346 | "height": 612, 347 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/634e57abbbba46adbff9950574d8b02f_7.jpg", 348 | "width": 612 349 | }, 350 | "thumbnail": { 351 | "height": 150, 352 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/634e57abbbba46adbff9950574d8b02f_5.jpg", 353 | "width": 150 354 | } 355 | }, 356 | "likes": { 357 | "count": 0, 358 | "data": [] 359 | }, 360 | "link": "http://instagr.am/p/B147p/", 361 | "location": { 362 | "id": "505254", 363 | "latitude": 25.782665000000001, 364 | "longitude": -80.128191999999999, 365 | "name": "The Beach at 12th" 366 | }, 367 | "tags": [], 368 | "type": "image", 369 | "user": { 370 | "full_name": "Kent Nguyen", 371 | "id": "33287", 372 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_33287_75sq_1287025465.jpg", 373 | "username": "kentnguyen" 374 | } 375 | }, 376 | { 377 | "caption": { 378 | "created_time": "1298667678", 379 | "from": { 380 | "full_name": "Kent Nguyen", 381 | "id": "33287", 382 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_33287_75sq_1287025465.jpg", 383 | "username": "kentnguyen" 384 | }, 385 | "id": "37970103", 386 | "text": "Warmth" 387 | }, 388 | "comments": { 389 | "count": 0, 390 | "data": [] 391 | }, 392 | "created_time": "1298667637", 393 | "filter": "Toaster", 394 | "id": "30900564", 395 | "images": { 396 | "low_resolution": { 397 | "height": 306, 398 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/5cddd82d19004e10a48ce3f79041d3c3_6.jpg", 399 | "width": 306 400 | }, 401 | "standard_resolution": { 402 | "height": 612, 403 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/5cddd82d19004e10a48ce3f79041d3c3_7.jpg", 404 | "width": 612 405 | }, 406 | "thumbnail": { 407 | "height": 150, 408 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/5cddd82d19004e10a48ce3f79041d3c3_5.jpg", 409 | "width": 150 410 | } 411 | }, 412 | "likes": { 413 | "count": 1, 414 | "data": [ 415 | { 416 | "full_name": "Aaron Lewis", 417 | "id": "10515", 418 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_10515_75sq_1286421290.jpg", 419 | "username": "aaronglewis" 420 | } 421 | ] 422 | }, 423 | "link": "http://instagr.am/p/B14FU/", 424 | "location": { 425 | "id": "505254", 426 | "latitude": 25.782665000000001, 427 | "longitude": -80.128191999999999, 428 | "name": "The Beach at 12th" 429 | }, 430 | "tags": [], 431 | "type": "image", 432 | "user": { 433 | "full_name": "Kent Nguyen", 434 | "id": "33287", 435 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_33287_75sq_1287025465.jpg", 436 | "username": "kentnguyen" 437 | } 438 | }, 439 | { 440 | "caption": { 441 | "created_time": "1298666842", 442 | "from": { 443 | "full_name": "", 444 | "id": "336510", 445 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_336510_75sq_1291233229.jpg", 446 | "username": "jdecker" 447 | }, 448 | "id": "37966056", 449 | "text": "Miami" 450 | }, 451 | "comments": { 452 | "count": 0, 453 | "data": [] 454 | }, 455 | "created_time": "1298666821", 456 | "filter": "Earlybird", 457 | "id": "30897420", 458 | "images": { 459 | "low_resolution": { 460 | "height": 306, 461 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/2fb2c2c0463b459eb5667ff920567864_6.jpg", 462 | "width": 306 463 | }, 464 | "standard_resolution": { 465 | "height": 612, 466 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/2fb2c2c0463b459eb5667ff920567864_7.jpg", 467 | "width": 612 468 | }, 469 | "thumbnail": { 470 | "height": 150, 471 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/2fb2c2c0463b459eb5667ff920567864_5.jpg", 472 | "width": 150 473 | } 474 | }, 475 | "likes": { 476 | "count": 0, 477 | "data": [] 478 | }, 479 | "link": "http://instagr.am/p/B13UM/", 480 | "location": { 481 | "id": "289408", 482 | "latitude": 25.784192999999998, 483 | "longitude": -80.130199000000005, 484 | "name": "South Beach Ocean" 485 | }, 486 | "tags": [], 487 | "type": "image", 488 | "user": { 489 | "full_name": "", 490 | "id": "336510", 491 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_336510_75sq_1291233229.jpg", 492 | "username": "jdecker" 493 | } 494 | }, 495 | { 496 | "caption": { 497 | "created_time": "1298665180", 498 | "from": { 499 | "full_name": "", 500 | "id": "2197230", 501 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 502 | "username": "cmcrosby" 503 | }, 504 | "id": "37957766", 505 | "text": "Miami" 506 | }, 507 | "comments": { 508 | "count": 0, 509 | "data": [] 510 | }, 511 | "created_time": "1298665147", 512 | "filter": "Earlybird", 513 | "id": "30890903", 514 | "images": { 515 | "low_resolution": { 516 | "height": 306, 517 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/03d8530c3b284d96925c9d1c6ed039e3_6.jpg", 518 | "width": 306 519 | }, 520 | "standard_resolution": { 521 | "height": 612, 522 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/03d8530c3b284d96925c9d1c6ed039e3_7.jpg", 523 | "width": 612 524 | }, 525 | "thumbnail": { 526 | "height": 150, 527 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/03d8530c3b284d96925c9d1c6ed039e3_5.jpg", 528 | "width": 150 529 | } 530 | }, 531 | "likes": { 532 | "count": 0, 533 | "data": [] 534 | }, 535 | "link": "http://instagr.am/p/B11uX/", 536 | "location": { 537 | "id": "356866", 538 | "latitude": 25.815430900286731, 539 | "longitude": -80.122459530830383, 540 | "name": "Four Points By Sheraton Miami Beach" 541 | }, 542 | "tags": [], 543 | "type": "image", 544 | "user": { 545 | "full_name": "", 546 | "id": "2197230", 547 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 548 | "username": "cmcrosby" 549 | } 550 | }, 551 | { 552 | "caption": { 553 | "created_time": "1298665069", 554 | "from": { 555 | "full_name": "", 556 | "id": "2197230", 557 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 558 | "username": "cmcrosby" 559 | }, 560 | "id": "37957191", 561 | "text": "Just chillin'" 562 | }, 563 | "comments": { 564 | "count": 0, 565 | "data": [] 566 | }, 567 | "created_time": "1298665044", 568 | "filter": "Earlybird", 569 | "id": "30890496", 570 | "images": { 571 | "low_resolution": { 572 | "height": 306, 573 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/215b5c264974433e9cd90a6ab6b87717_6.jpg", 574 | "width": 306 575 | }, 576 | "standard_resolution": { 577 | "height": 612, 578 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/215b5c264974433e9cd90a6ab6b87717_7.jpg", 579 | "width": 612 580 | }, 581 | "thumbnail": { 582 | "height": 150, 583 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/215b5c264974433e9cd90a6ab6b87717_5.jpg", 584 | "width": 150 585 | } 586 | }, 587 | "likes": { 588 | "count": 0, 589 | "data": [] 590 | }, 591 | "link": "http://instagr.am/p/B11oA/", 592 | "location": { 593 | "id": "356866", 594 | "latitude": 25.815430900286731, 595 | "longitude": -80.122459530830383, 596 | "name": "Four Points By Sheraton Miami Beach" 597 | }, 598 | "tags": [], 599 | "type": "image", 600 | "user": { 601 | "full_name": "", 602 | "id": "2197230", 603 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 604 | "username": "cmcrosby" 605 | } 606 | }, 607 | { 608 | "caption": { 609 | "created_time": "1298663506", 610 | "from": { 611 | "full_name": "Tyler Hunt", 612 | "id": "188651", 613 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 614 | "username": "tylerhunt" 615 | }, 616 | "id": "37949493", 617 | "text": "Winter in Florida" 618 | }, 619 | "comments": { 620 | "count": 0, 621 | "data": [] 622 | }, 623 | "created_time": "1298663503", 624 | "filter": "X-Pro II", 625 | "id": "30884692", 626 | "images": { 627 | "low_resolution": { 628 | "height": 306, 629 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/693823b891534f2d8de5f7b6ed5295ab_6.jpg", 630 | "width": 306 631 | }, 632 | "standard_resolution": { 633 | "height": 612, 634 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/693823b891534f2d8de5f7b6ed5295ab_7.jpg", 635 | "width": 612 636 | }, 637 | "thumbnail": { 638 | "height": 150, 639 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/693823b891534f2d8de5f7b6ed5295ab_5.jpg", 640 | "width": 150 641 | } 642 | }, 643 | "likes": { 644 | "count": 0, 645 | "data": [] 646 | }, 647 | "link": "http://instagr.am/p/B10NU/", 648 | "location": { 649 | "id": "285438", 650 | "latitude": 25.790564, 651 | "longitude": -80.136550999999997, 652 | "name": "Lincoln Road Mall" 653 | }, 654 | "tags": [], 655 | "type": "image", 656 | "user": { 657 | "full_name": "Tyler Hunt", 658 | "id": "188651", 659 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 660 | "username": "tylerhunt" 661 | } 662 | }, 663 | { 664 | "caption": { 665 | "created_time": "1298661746", 666 | "from": { 667 | "full_name": "Eric Allam", 668 | "id": "5733", 669 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_5733_75sq_1286458838.jpg", 670 | "username": "rubymaverick" 671 | }, 672 | "id": "37940900", 673 | "text": "Walking down Lincoln road" 674 | }, 675 | "comments": { 676 | "count": 0, 677 | "data": [] 678 | }, 679 | "created_time": "1298661723", 680 | "filter": "Earlybird", 681 | "id": "30878142", 682 | "images": { 683 | "low_resolution": { 684 | "height": 306, 685 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/0d19be935a6b4c1a906334c37ad23851_6.jpg", 686 | "width": 306 687 | }, 688 | "standard_resolution": { 689 | "height": 612, 690 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/0d19be935a6b4c1a906334c37ad23851_7.jpg", 691 | "width": 612 692 | }, 693 | "thumbnail": { 694 | "height": 150, 695 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/0d19be935a6b4c1a906334c37ad23851_5.jpg", 696 | "width": 150 697 | } 698 | }, 699 | "likes": { 700 | "count": 1, 701 | "data": [ 702 | { 703 | "full_name": "", 704 | "id": "333049", 705 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 706 | "username": "eklosterman" 707 | } 708 | ] 709 | }, 710 | "link": "http://instagr.am/p/B1ym-/", 711 | "location": { 712 | "id": "285438", 713 | "latitude": 25.790564, 714 | "longitude": -80.136550999999997, 715 | "name": "Lincoln Road Mall" 716 | }, 717 | "tags": [], 718 | "type": "image", 719 | "user": { 720 | "full_name": "Eric Allam", 721 | "id": "5733", 722 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_5733_75sq_1286458838.jpg", 723 | "username": "rubymaverick" 724 | } 725 | }, 726 | { 727 | "caption": { 728 | "created_time": "1298659226", 729 | "from": { 730 | "full_name": "", 731 | "id": "2195617", 732 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 733 | "username": "misssparker" 734 | }, 735 | "id": "37928792", 736 | "text": "The Standard" 737 | }, 738 | "comments": { 739 | "count": 0, 740 | "data": [] 741 | }, 742 | "created_time": "1298659215", 743 | "filter": "Toaster", 744 | "id": "30868862", 745 | "images": { 746 | "low_resolution": { 747 | "height": 306, 748 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/09a70957c03e486a93648d71540df065_6.jpg", 749 | "width": 306 750 | }, 751 | "standard_resolution": { 752 | "height": 612, 753 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/09a70957c03e486a93648d71540df065_7.jpg", 754 | "width": 612 755 | }, 756 | "thumbnail": { 757 | "height": 150, 758 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/09a70957c03e486a93648d71540df065_5.jpg", 759 | "width": 150 760 | } 761 | }, 762 | "likes": { 763 | "count": 0, 764 | "data": [] 765 | }, 766 | "link": "http://instagr.am/p/B1wV-/", 767 | "location": { 768 | "id": "220744", 769 | "latitude": 25.792287999999999, 770 | "longitude": -80.149286000000004, 771 | "name": "The Standard Miami" 772 | }, 773 | "tags": [], 774 | "type": "image", 775 | "user": { 776 | "full_name": "", 777 | "id": "2195617", 778 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 779 | "username": "misssparker" 780 | } 781 | }, 782 | { 783 | "caption": null, 784 | "comments": { 785 | "count": 1, 786 | "data": [ 787 | { 788 | "created_time": "1298671739", 789 | "from": { 790 | "full_name": "Stephany Ospina", 791 | "id": "236852", 792 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_236852_75sq_1292203189.jpg", 793 | "username": "stephospina" 794 | }, 795 | "id": "37992465", 796 | "text": "\"fitted attire\" lol" 797 | } 798 | ] 799 | }, 800 | "created_time": "1298658189", 801 | "filter": null, 802 | "id": "30864690", 803 | "images": { 804 | "low_resolution": { 805 | "height": 306, 806 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/ba365bc2e90049ae826db46468b0c4d1_6.jpg", 807 | "width": 306 808 | }, 809 | "standard_resolution": { 810 | "height": 612, 811 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/ba365bc2e90049ae826db46468b0c4d1_7.jpg", 812 | "width": 612 813 | }, 814 | "thumbnail": { 815 | "height": 150, 816 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/ba365bc2e90049ae826db46468b0c4d1_5.jpg", 817 | "width": 150 818 | } 819 | }, 820 | "likes": { 821 | "count": 1, 822 | "data": [ 823 | { 824 | "full_name": "Andrea Cupcake", 825 | "id": "1130204", 826 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1130204_75sq_1293193717.jpg", 827 | "username": "koshercupcake" 828 | } 829 | ] 830 | }, 831 | "link": "http://instagr.am/p/B1vUy/", 832 | "location": { 833 | "id": "632073", 834 | "latitude": 25.791796000000001, 835 | "longitude": -80.138589999999994, 836 | "name": "Lucky Strikes Lanes & Lounge" 837 | }, 838 | "tags": [], 839 | "type": "image", 840 | "user": { 841 | "full_name": "Roberto Martinez", 842 | "id": "124346", 843 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_124346_75sq_1296666430.jpg", 844 | "username": "amazinrobert" 845 | } 846 | }, 847 | { 848 | "caption": { 849 | "created_time": "1298656945", 850 | "from": { 851 | "full_name": "", 852 | "id": "2165267", 853 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_2165267_75sq_1298657401.jpg", 854 | "username": "nicolediodonet" 855 | }, 856 | "id": "37916530", 857 | "text": "ART" 858 | }, 859 | "comments": { 860 | "count": 0, 861 | "data": [] 862 | }, 863 | "created_time": "1298656642", 864 | "filter": "Lomo-fi", 865 | "id": "30858567", 866 | "images": { 867 | "low_resolution": { 868 | "height": 306, 869 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/0c533b470ac74ddd827625f4274290c9_6.jpg", 870 | "width": 306 871 | }, 872 | "standard_resolution": { 873 | "height": 612, 874 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/0c533b470ac74ddd827625f4274290c9_7.jpg", 875 | "width": 612 876 | }, 877 | "thumbnail": { 878 | "height": 150, 879 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/0c533b470ac74ddd827625f4274290c9_5.jpg", 880 | "width": 150 881 | } 882 | }, 883 | "likes": { 884 | "count": 0, 885 | "data": [] 886 | }, 887 | "link": null, 888 | "location": { 889 | "id": "958155", 890 | "latitude": 25.790111, 891 | "longitude": -80.136858000000004, 892 | "name": "Art Center South Florida" 893 | }, 894 | "tags": [], 895 | "type": "image", 896 | "user": { 897 | "full_name": "", 898 | "id": "2165267", 899 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_2165267_75sq_1298657401.jpg", 900 | "username": "nicolediodonet" 901 | } 902 | }, 903 | { 904 | "caption": { 905 | "created_time": "1298656311", 906 | "from": { 907 | "full_name": "", 908 | "id": "2165267", 909 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_2165267_75sq_1298657401.jpg", 910 | "username": "nicolediodonet" 911 | }, 912 | "id": "37913025", 913 | "text": "Sea Creatures " 914 | }, 915 | "comments": { 916 | "count": 0, 917 | "data": [] 918 | }, 919 | "created_time": "1298656242", 920 | "filter": "Lomo-fi", 921 | "id": "30856946", 922 | "images": { 923 | "low_resolution": { 924 | "height": 306, 925 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/9029c7277319443e9a59ca7d67615aae_6.jpg", 926 | "width": 306 927 | }, 928 | "standard_resolution": { 929 | "height": 612, 930 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/9029c7277319443e9a59ca7d67615aae_7.jpg", 931 | "width": 612 932 | }, 933 | "thumbnail": { 934 | "height": 150, 935 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/9029c7277319443e9a59ca7d67615aae_5.jpg", 936 | "width": 150 937 | } 938 | }, 939 | "likes": { 940 | "count": 0, 941 | "data": [] 942 | }, 943 | "link": null, 944 | "location": { 945 | "id": "958155", 946 | "latitude": 25.790111, 947 | "longitude": -80.136858000000004, 948 | "name": "Art Center South Florida" 949 | }, 950 | "tags": [], 951 | "type": "image", 952 | "user": { 953 | "full_name": "", 954 | "id": "2165267", 955 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_2165267_75sq_1298657401.jpg", 956 | "username": "nicolediodonet" 957 | } 958 | }, 959 | { 960 | "caption": { 961 | "created_time": "1298656177", 962 | "from": { 963 | "full_name": "Tanu Suri", 964 | "id": "215250", 965 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_215250_75sq_1287677743.jpg", 966 | "username": "tanu" 967 | }, 968 | "id": "37912233", 969 | "text": "Beautiful afternoon in Miami!" 970 | }, 971 | "comments": { 972 | "count": 0, 973 | "data": [] 974 | }, 975 | "created_time": "1298656131", 976 | "filter": "Lomo-fi", 977 | "id": "30856500", 978 | "images": { 979 | "low_resolution": { 980 | "height": 306, 981 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/348b69e779f14426b47aa0472b839f96_6.jpg", 982 | "width": 306 983 | }, 984 | "standard_resolution": { 985 | "height": 612, 986 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/348b69e779f14426b47aa0472b839f96_7.jpg", 987 | "width": 612 988 | }, 989 | "thumbnail": { 990 | "height": 150, 991 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/348b69e779f14426b47aa0472b839f96_5.jpg", 992 | "width": 150 993 | } 994 | }, 995 | "likes": { 996 | "count": 0, 997 | "data": [] 998 | }, 999 | "link": "http://instagr.am/p/B1tU0/", 1000 | "location": { 1001 | "id": "1517948", 1002 | "latitude": 25.851096999999999, 1003 | "longitude": -80.119667000000007, 1004 | "name": "Deauville Hotel" 1005 | }, 1006 | "tags": [], 1007 | "type": "image", 1008 | "user": { 1009 | "full_name": "Tanu Suri", 1010 | "id": "215250", 1011 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_215250_75sq_1287677743.jpg", 1012 | "username": "tanu" 1013 | } 1014 | }, 1015 | { 1016 | "caption": { 1017 | "created_time": "1298655780", 1018 | "from": { 1019 | "full_name": "", 1020 | "id": "2197230", 1021 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 1022 | "username": "cmcrosby" 1023 | }, 1024 | "id": "37909881", 1025 | "text": "Lincoln road, south beach" 1026 | }, 1027 | "comments": { 1028 | "count": 0, 1029 | "data": [] 1030 | }, 1031 | "created_time": "1298655778", 1032 | "filter": "Earlybird", 1033 | "id": "30855064", 1034 | "images": { 1035 | "low_resolution": { 1036 | "height": 306, 1037 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/f672982696654db18e053294766a9565_6.jpg", 1038 | "width": 306 1039 | }, 1040 | "standard_resolution": { 1041 | "height": 612, 1042 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/f672982696654db18e053294766a9565_7.jpg", 1043 | "width": 612 1044 | }, 1045 | "thumbnail": { 1046 | "height": 150, 1047 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/f672982696654db18e053294766a9565_5.jpg", 1048 | "width": 150 1049 | } 1050 | }, 1051 | "likes": { 1052 | "count": 0, 1053 | "data": [] 1054 | }, 1055 | "link": "http://instagr.am/p/B1s-Y/", 1056 | "location": { 1057 | "id": "433631", 1058 | "latitude": 25.790199999999999, 1059 | "longitude": -80.137799999999999, 1060 | "name": "Pasha's" 1061 | }, 1062 | "tags": [], 1063 | "type": "image", 1064 | "user": { 1065 | "full_name": "", 1066 | "id": "2197230", 1067 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/anonymousUser.jpg", 1068 | "username": "cmcrosby" 1069 | } 1070 | }, 1071 | { 1072 | "caption": { 1073 | "created_time": "1298655661", 1074 | "from": { 1075 | "full_name": "", 1076 | "id": "2165267", 1077 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_2165267_75sq_1298657401.jpg", 1078 | "username": "nicolediodonet" 1079 | }, 1080 | "id": "37909161", 1081 | "text": "Morning fireworks " 1082 | }, 1083 | "comments": { 1084 | "count": 0, 1085 | "data": [] 1086 | }, 1087 | "created_time": "1298655620", 1088 | "filter": "Lomo-fi", 1089 | "id": "30854426", 1090 | "images": { 1091 | "low_resolution": { 1092 | "height": 306, 1093 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/12053a3e571f4c26951f573f13a7adf6_6.jpg", 1094 | "width": 306 1095 | }, 1096 | "standard_resolution": { 1097 | "height": 612, 1098 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/12053a3e571f4c26951f573f13a7adf6_7.jpg", 1099 | "width": 612 1100 | }, 1101 | "thumbnail": { 1102 | "height": 150, 1103 | "url": "http://distillery.s3.amazonaws.com/media/2011/02/25/12053a3e571f4c26951f573f13a7adf6_5.jpg", 1104 | "width": 150 1105 | } 1106 | }, 1107 | "likes": { 1108 | "count": 1, 1109 | "data": [ 1110 | { 1111 | "full_name": "", 1112 | "id": "1639851", 1113 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_1639851_75sq_1295812928.jpg", 1114 | "username": "diorsantos" 1115 | } 1116 | ] 1117 | }, 1118 | "link": null, 1119 | "location": { 1120 | "id": "567181", 1121 | "latitude": 25.8039094166182, 1122 | "longitude": -80.128097534179688, 1123 | "name": "South Beach, Miami" 1124 | }, 1125 | "tags": [], 1126 | "type": "image", 1127 | "user": { 1128 | "full_name": "", 1129 | "id": "2165267", 1130 | "profile_picture": "http://distillery.s3.amazonaws.com/profiles/profile_2165267_75sq_1298657401.jpg", 1131 | "username": "nicolediodonet" 1132 | } 1133 | } 1134 | ], 1135 | "meta": { 1136 | "code": 200 1137 | }, 1138 | "pagination": { 1139 | "next_max_id": "30854426", 1140 | "next_url": "https://api.instagram.com/v1/geographies/106/media/recent?max_id=30854426&client_id=XXXXXX" 1141 | } 1142 | } 1143 | --------------------------------------------------------------------------------