Compare commits

...
19 Commits
Author SHA1 Message Date
André Koch-Kramer 440c59ecb9 Release of version 3.3.10 2018-04-20 18:51:55 +02:00
André Koch-Kramer c9198d232f Add 'disable=protected-access' to satisfy pylint 2018-04-20 18:47:36 +02:00
André Koch-Kramer e689dadf00 Output warning when fetching special data
When fetching comments, likes, followees or followers, output a warning
that the used queries are rate limited.
2018-04-20 15:34:43 +02:00
André Koch-Kramer d90c05e0a4 Limit GraphQl queries to 20 per 11 minutes
- Set GRAPHQL_PAGE_LENGTH to 50 what appears to be the new working maximum.
- Limit GQL queries to 20 per 666 seconds.
- Remove logic for tracking queries per query identifier as Instagram
  only allows 20 overall GQL queries per sliding window.

Related to #101
2018-04-20 12:59:41 +02:00
Alexander Graf eecaca7c7b Release of version 3.3.9 2018-04-18 16:25:53 +02:00
Alexander Graf 729aa31d86 Reverse story download order to enable fast-update
Fixes #100.
2018-04-18 16:24:47 +02:00
Alexander Graf 94df60292e Fix bug about 429 handling
It caused Instaloader to fail with a TypeError on 429 Too Many Requests.
2018-04-18 16:24:20 +02:00
Alexander Graf 09f2eb3601 Set GRAPHQL_PAGE_LENGTH to 12
This is the length the browser requests. Might alleviate #94.
2018-04-18 11:41:00 +02:00
Alexander Graf 3e904e8a14 Release of version 3.3.8 2018-04-17 13:30:45 +02:00
Alexander Graf f16b99bec0 Fix GraphQL Queries (again)
Fixes #96.
2018-04-17 13:17:50 +02:00
Alexander Graf d316ddf02a Release of version 3.3.7 2018-04-13 18:17:40 +02:00
Alexander Graf efa0b93fda Fix rhx_gis calculation to fix 403 (fix #94) 2018-04-13 18:05:36 +02:00
André Koch-Kramer ced65ea401 Release of version 3.3.6 2018-04-11 14:32:42 +02:00
André Koch-Kramer 73ec884ea4 Fix anonymous GraphQL queries
Closes #94.
2018-04-11 14:19:24 +02:00
Alexander Graf 74d5e35eb8 get_{comments,likes}: save requests
Post.get_{comments,likes} had a logic to save requests if comments/like
count is low. That logic had a bug that is now fixed.

Related to #94.
2018-04-11 11:24:49 +02:00
Alexander Graf 8612712f44 Release of version 3.3.5 2018-04-10 20:57:53 +02:00
Alexander Graf 67ac8f3397 Enforce being logged in for getting username by id
It seems it is not possible at the moment to get username by userid
anonymously.
2018-04-10 20:52:39 +02:00
Alexander Graf 5fe2a70374 Fix downloading of hashtags 2018-04-10 20:34:07 +02:00
Alexander Graf 8bd9c4449c Theme-related Documentation Enhancements
- Consistent background color in all types of codes
- Improve dd (list item) padding
- Link to Index in global TOC in navbar
- Briefly describe Instaloader in `index.html` title
- Remove page contents (navbartoc) in index, genindex and installation
- Link to new pypi.org rather than old pypi.python.org
2018-04-09 16:07:08 +02:00
7 changed files with 124 additions and 57 deletions
+10 -1
View File
@@ -1,3 +1,12 @@
code { code {
color: #222; color: #222;
} background-color: #f5f5f5;
}
dd {
padding-left: 8px;
}
dl.option dd {
padding-left: 12px;
}
+14
View File
@@ -0,0 +1,14 @@
<li class="dropdown globaltoc-container">
<a role="button"
id="dLabelGlobalToc"
data-toggle="dropdown"
data-target="#"
href="{{ pathto(master_doc) }}">{{ theme_navbar_site_name }} <b class="caret"></b></a>
<ul class="dropdown-menu globaltoc"
role="menu"
aria-labelledby="dLabelGlobalToc">
{{ toctree(maxdepth=theme_globaltoc_depth|toint, collapse=False, includehidden=theme_globaltoc_includehidden|tobool) }}
<li role="separator" class="divider"></li>
<li class="toctree-l1"><a class="reference internal" href="genindex.html">Index</a></li>
</ul>
</li>
+7
View File
@@ -1,4 +1,11 @@
{% extends "!layout.html" %} {% extends "!layout.html" %}
{% block htmltitle %}
{% if pagename == "index" %}
<title>{{ title|striptags|e }}</title>
{% else %}
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
{% endif %}
{% endblock %}
{% block extrahead %} {% block extrahead %}
{{ super() }} {{ super() }}
<meta name="google-site-verification" content="FNUsDdkOJM9obm9QEdrNBVd9AtAzHzA-aaSMCUrS6C8" /> <meta name="google-site-verification" content="FNUsDdkOJM9obm9QEdrNBVd9AtAzHzA-aaSMCUrS6C8" />
+3 -3
View File
@@ -14,7 +14,7 @@
{% if theme_navbar_title -%}{{ theme_navbar_title|e }}{%- else -%}{{ project|e }}{%- endif -%} {% if theme_navbar_title -%}{{ theme_navbar_title|e }}{%- else -%}{{ project|e }}{%- endif -%}
</a> </a>
<span class="navbar-text navbar-version pull-left"> <span class="navbar-text navbar-version pull-left">
<a href="https://github.com/instaloader/instaloader/releases/tag/v{{current_release}}" title="Released on {{current_release_date}}.">v<b>{{current_release}}</b></a> <a href="https://github.com/instaloader/instaloader/releases/tag/v{{current_release}}" title="Released on {{current_release_date}}">v<b>{{current_release}}</b></a>
</span> </span>
</div> </div>
@@ -27,7 +27,7 @@
{% endif %} {% endif %}
{% block navbartoc %} {% block navbartoc %}
{% include "globaltoc.html" %} {% include "globaltoc.html" %}
{% if theme_navbar_pagenav %} {% if theme_navbar_pagenav and pagename not in ("index", "genindex", "installation") %}
{% include "navbartoc.html" %} {% include "navbartoc.html" %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
@@ -46,7 +46,7 @@
<ul class="dropdown-menu" role="menu" aria-labelledby="dLabelUsefulLinks"> <ul class="dropdown-menu" role="menu" aria-labelledby="dLabelUsefulLinks">
<li><a href="https://github.com/instaloader/instaloader/issues">Issue Tracker</a></li> <li><a href="https://github.com/instaloader/instaloader/issues">Issue Tracker</a></li>
<li><a href="https://github.com/instaloader/instaloader/releases">Version History</a></li> <li><a href="https://github.com/instaloader/instaloader/releases">Version History</a></li>
<li><a href="https://pypi.python.org/pypi/instaloader">Instaloader on PyPI</a></li> <li><a href="https://pypi.org/project/instaloader/">Instaloader on PyPI</a></li>
<li><a href="https://github.com/instaloader/instaloader">Instaloader on GitHub</a></li> <li><a href="https://github.com/instaloader/instaloader">Instaloader on GitHub</a></li>
<li role="separator" class="divider"></li> <li role="separator" class="divider"></li>
<li style="padding-left: 20px;"><a class="github-button" href="https://github.com/instaloader/instaloader" data-icon="octicon-star" data-size="large" data-show-count="true" aria-label="Star Instaloader on GitHub">Star</a></li> <li style="padding-left: 20px;"><a class="github-button" href="https://github.com/instaloader/instaloader" data-icon="octicon-star" data-size="large" data-show-count="true" aria-label="Star Instaloader on GitHub">Star</a></li>
+1 -1
View File
@@ -212,7 +212,7 @@ html_domain_indices = False
# If false, no index is generated. # If false, no index is generated.
# #
html_use_index = False html_use_index = True
# If true, the index is split into individual pages for each letter. # If true, the index is split into individual pages for each letter.
# #
+5 -3
View File
@@ -5,6 +5,8 @@
saved media, and their metadata, comments and captions. saved media, and their metadata, comments and captions.
Written in Python. Written in Python.
.. title:: Instaloader — Download Instagram Photos and Metadata
Instaloader Instaloader
=========== ===========
@@ -43,8 +45,8 @@ With `Python <https://www.python.org/>`__ installed, do::
:stories | :feed | :saved :stories | :feed | :saved
Table of Contents Instaloader Documentation
----------------- -------------------------
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
@@ -59,7 +61,7 @@ Useful Links
------------ ------------
- `Git Repository (on GitHub) <https://github.com/instaloader/instaloader>`__ - `Git Repository (on GitHub) <https://github.com/instaloader/instaloader>`__
- `PyPI Project Page <https://pypi.python.org/pypi/instaloader>`__ - `PyPI Project Page <https://pypi.org/project/instaloader/>`__
- `Issue Tracker / Bug Tracker <https://github.com/instaloader/instaloader/issues>`__ - `Issue Tracker / Bug Tracker <https://github.com/instaloader/instaloader/issues>`__
- `Version History <https://github.com/instaloader/instaloader/releases>`__ - `Version History <https://github.com/instaloader/instaloader/releases>`__
+84 -49
View File
@@ -3,6 +3,7 @@
"""Download pictures (or videos) along with their captions and other metadata from Instagram.""" """Download pictures (or videos) along with their captions and other metadata from Instagram."""
import ast import ast
import getpass import getpass
import hashlib
import json import json
import os import os
import pickle import pickle
@@ -30,7 +31,7 @@ import requests.utils
import urllib3 import urllib3
__version__ = '3.3.4' __version__ = '3.3.10'
# NOTE: duplicated in README.rst and docs/index.rst # NOTE: duplicated in README.rst and docs/index.rst
USAGE_STRING = """ USAGE_STRING = """
@@ -201,6 +202,7 @@ class Post:
self._profile = profile self._profile = profile
self._profile_id = profile_id self._profile_id = profile_id
self._full_metadata_dict = None self._full_metadata_dict = None
self._rhx_gis = None
@classmethod @classmethod
def from_shortcode(cls, instaloader: 'Instaloader', shortcode: str): def from_shortcode(cls, instaloader: 'Instaloader', shortcode: str):
@@ -239,11 +241,9 @@ class Post:
@property @property
def _full_metadata(self) -> Dict[str, Any]: def _full_metadata(self) -> Dict[str, Any]:
if not self._full_metadata_dict: if not self._full_metadata_dict:
pic_json = self._instaloader.get_json("p/{0}/".format(self.shortcode), params={'__a': 1}) pic_json = self._instaloader.get_json("p/{0}/".format(self.shortcode), params={})
if "graphql" in pic_json: self._full_metadata_dict = pic_json['entry_data']['PostPage'][0]['graphql']['shortcode_media']
self._full_metadata_dict = pic_json["graphql"]["shortcode_media"] self._rhx_gis = pic_json['rhx_gis']
else:
self._full_metadata_dict = pic_json["media"]
return self._full_metadata_dict return self._full_metadata_dict
def _field(self, *keys) -> Any: def _field(self, *keys) -> Any:
@@ -377,6 +377,7 @@ class Post:
Each comment is represented by a dictionary having the keys text, created_at, id and owner, which is a Each comment is represented by a dictionary having the keys text, created_at, id and owner, which is a
dictionary with keys username, profile_pic_url and id. dictionary with keys username, profile_pic_url and id.
""" """
# pylint:disable=protected-access
if self.comments == 0: if self.comments == 0:
# Avoid doing additional requests if there are no comments # Avoid doing additional requests if there are no comments
return return
@@ -384,9 +385,12 @@ class Post:
if self.comments == len(comment_edges): if self.comments == len(comment_edges):
# If the Post's metadata already contains all comments, don't do GraphQL requests to obtain them # If the Post's metadata already contains all comments, don't do GraphQL requests to obtain them
yield from (comment['node'] for comment in comment_edges) yield from (comment['node'] for comment in comment_edges)
return
self._instaloader._log('(Warning: Fetching comments uses many queries, which are rate limited.)')
yield from self._instaloader.graphql_node_list(17852405266163336, {'shortcode': self.shortcode}, yield from self._instaloader.graphql_node_list(17852405266163336, {'shortcode': self.shortcode},
'https://www.instagram.com/p/' + self.shortcode + '/', 'https://www.instagram.com/p/' + self.shortcode + '/',
lambda d: d['data']['shortcode_media']['edge_media_to_comment']) lambda d: d['data']['shortcode_media']['edge_media_to_comment'],
rhx_gis=self._rhx_gis)
def get_likes(self) -> Iterator[Dict[str, Any]]: def get_likes(self) -> Iterator[Dict[str, Any]]:
"""Iterate over all likes of the post. """Iterate over all likes of the post.
@@ -394,6 +398,7 @@ class Post:
Each like is represented by a dictionary having the keys username, followed_by_viewer, id, is_verified, Each like is represented by a dictionary having the keys username, followed_by_viewer, id, is_verified,
requested_by_viewer, followed_by_viewer, profile_pic_url. requested_by_viewer, followed_by_viewer, profile_pic_url.
""" """
# pylint:disable=protected-access
if self.likes == 0: if self.likes == 0:
# Avoid doing additional requests if there are no comments # Avoid doing additional requests if there are no comments
return return
@@ -401,9 +406,12 @@ class Post:
if self.likes == len(likes_edges): if self.likes == len(likes_edges):
# If the Post's metadata already contains all likes, don't do GraphQL requests to obtain them # If the Post's metadata already contains all likes, don't do GraphQL requests to obtain them
yield from (like['node'] for like in likes_edges) yield from (like['node'] for like in likes_edges)
return
self._instaloader._log('(Warning: Fetching likes uses many queries, which are rate limited.)')
yield from self._instaloader.graphql_node_list("1cb6ec562846122743b61e492c85999f", {'shortcode': self.shortcode}, yield from self._instaloader.graphql_node_list("1cb6ec562846122743b61e492c85999f", {'shortcode': self.shortcode},
'https://www.instagram.com/p/' + self.shortcode + '/', 'https://www.instagram.com/p/' + self.shortcode + '/',
lambda d: d['data']['shortcode_media']['edge_liked_by']) lambda d: d['data']['shortcode_media']['edge_liked_by'],
rhx_gis=self._rhx_gis)
def get_location(self) -> Optional[Dict[str, str]]: def get_location(self) -> Optional[Dict[str, str]]:
"""If the Post has a location, returns a dictionary with fields 'lat' and 'lng'.""" """If the Post has a location, returns a dictionary with fields 'lat' and 'lng'."""
@@ -456,7 +464,7 @@ class _PathPattern(str):
class Instaloader: class Instaloader:
GRAPHQL_PAGE_LENGTH = 200 GRAPHQL_PAGE_LENGTH = 50
def __init__(self, def __init__(self,
sleep: bool = True, quiet: bool = False, sleep: bool = True, quiet: bool = False,
@@ -502,7 +510,7 @@ class Instaloader:
self.error_log = [] self.error_log = []
# For the adaption of sleep intervals (rate control) # For the adaption of sleep intervals (rate control)
self.previous_queries = dict() self.query_timestamps = list()
@property @property
def is_logged_in(self) -> bool: def is_logged_in(self) -> bool:
@@ -519,10 +527,10 @@ class Instaloader:
self.download_geotags, self.download_geotags,
self.save_captions, self.download_comments, self.save_captions, self.download_comments,
self.save_metadata, self.max_connection_attempts) self.save_metadata, self.max_connection_attempts)
new_loader.previous_queries = self.previous_queries new_loader.query_timestamps = self.query_timestamps
yield new_loader yield new_loader
self.error_log.extend(new_loader.error_log) self.error_log.extend(new_loader.error_log)
self.previous_queries = new_loader.previous_queries self.query_timestamps = new_loader.query_timestamps
def _log(self, *msg, sep='', end='\n', flush=False): def _log(self, *msg, sep='', end='\n', flush=False):
"""Log a message to stdout that can be suppressed with --quiet.""" """Log a message to stdout that can be suppressed with --quiet."""
@@ -600,29 +608,25 @@ class Instaloader:
:raises QueryReturnedNotFoundException: When the server responds with a 404. :raises QueryReturnedNotFoundException: When the server responds with a 404.
:raises ConnectionException: When query repeatedly failed. :raises ConnectionException: When query repeatedly failed.
""" """
def graphql_query_waittime(query_id: int, untracked_queries: bool = False) -> int: def graphql_query_waittime(untracked_queries: bool = False) -> int:
sliding_window = 660 sliding_window = 660
timestamps = self.previous_queries.get(query_id) if not self.query_timestamps:
if not timestamps:
return sliding_window if untracked_queries else 0 return sliding_window if untracked_queries else 0
current_time = time.monotonic() current_time = time.monotonic()
timestamps = list(filter(lambda t: t > current_time - sliding_window, timestamps)) self.query_timestamps = list(filter(lambda t: t > current_time - sliding_window, self.query_timestamps))
self.previous_queries[query_id] = timestamps if len(self.query_timestamps) < 20 and not untracked_queries:
if len(timestamps) < 100 and not untracked_queries:
return 0 return 0
return round(min(timestamps) + sliding_window - current_time) + 6 return round(min(self.query_timestamps) + sliding_window - current_time) + 6
is_graphql_query = 'query_id' in params and 'graphql/query' in path is_graphql_query = 'graphql/query' in path
if is_graphql_query: if is_graphql_query:
query_id = params['query_id'] waittime = graphql_query_waittime()
waittime = graphql_query_waittime(query_id)
if waittime > 0: if waittime > 0:
self._log('\nToo many queries in the last time. Need to wait {} seconds.'.format(waittime)) self._log('\nToo many queries in the last time. Need to wait {} seconds.'.format(waittime))
time.sleep(waittime) time.sleep(waittime)
timestamp_list = self.previous_queries.get(query_id) if self.query_timestamps is not None:
if timestamp_list is not None: self.query_timestamps.append(time.monotonic())
timestamp_list.append(time.monotonic())
else: else:
self.previous_queries[query_id] = [time.monotonic()] self.query_timestamps = [time.monotonic()]
sess = session if session else self.session sess = session if session else self.session
try: try:
self._sleep() self._sleep()
@@ -633,7 +637,13 @@ class Instaloader:
raise TooManyRequests("429 - Too Many Requests") raise TooManyRequests("429 - Too Many Requests")
if resp.status_code != 200: if resp.status_code != 200:
raise ConnectionException("HTTP error code {}.".format(resp.status_code)) raise ConnectionException("HTTP error code {}.".format(resp.status_code))
resp_json = resp.json() if not is_graphql_query and not "__a" in params and host == "www.instagram.com":
match = re.search(r'window\._sharedData = (.*);</script>', resp.text)
if match is None:
raise ConnectionException("Could not find \"window._sharedData\" in html response.")
return json.loads(match.group(1))
else:
resp_json = resp.json()
if 'status' in resp_json and resp_json['status'] != "ok": if 'status' in resp_json and resp_json['status'] != "ok":
if 'message' in resp_json: if 'message' in resp_json:
raise ConnectionException("Returned \"{}\" status, message \"{}\".".format(resp_json['status'], raise ConnectionException("Returned \"{}\" status, message \"{}\".".format(resp_json['status'],
@@ -653,7 +663,7 @@ class Instaloader:
if isinstance(err, TooManyRequests): if isinstance(err, TooManyRequests):
print(textwrap.fill(text_for_429), file=sys.stderr) print(textwrap.fill(text_for_429), file=sys.stderr)
if is_graphql_query: if is_graphql_query:
waittime = graphql_query_waittime(query_id=params['query_id'], untracked_queries=True) waittime = graphql_query_waittime(untracked_queries=True)
if waittime > 0: if waittime > 0:
self._log('The request will be retried in {} seconds.'.format(waittime)) self._log('The request will be retried in {} seconds.'.format(waittime))
time.sleep(waittime) time.sleep(waittime)
@@ -693,7 +703,7 @@ class Instaloader:
return session return session
def graphql_query(self, query_identifier: Union[int, str], variables: Dict[str, Any], def graphql_query(self, query_identifier: Union[int, str], variables: Dict[str, Any],
referer: Optional[str] = None) -> Dict[str, Any]: referer: Optional[str] = None, rhx_gis: Optional[str] = None) -> Dict[str, Any]:
""" """
Do a GraphQL Query. Do a GraphQL Query.
@@ -711,9 +721,18 @@ class Instaloader:
tmpsession.headers['accept'] = '*/*' tmpsession.headers['accept'] = '*/*'
if referer is not None: if referer is not None:
tmpsession.headers['referer'] = urllib.parse.quote(referer) tmpsession.headers['referer'] = urllib.parse.quote(referer)
variables_json = json.dumps(variables, separators=(',', ':'))
if rhx_gis:
values = "{}:{}".format(rhx_gis, variables_json)
x_instagram_gis = hashlib.md5(values.encode()).hexdigest()
tmpsession.cookies.set('ig_pr', '2')
tmpsession.headers['x-instagram-gis'] = x_instagram_gis
resp_json = self.get_json('graphql/query', resp_json = self.get_json('graphql/query',
params={'query_id' if isinstance(query_identifier, int) else 'query_hash': query_identifier, params={'query_id' if isinstance(query_identifier, int) else 'query_hash': query_identifier,
'variables': json.dumps(variables, separators=(',', ':'))}, 'variables': variables_json},
session=tmpsession) session=tmpsession)
if 'status' not in resp_json: if 'status' not in resp_json:
self.error("GraphQL response did not contain a \"status\" field.") self.error("GraphQL response did not contain a \"status\" field.")
@@ -738,20 +757,21 @@ class Instaloader:
def get_id_by_username(self, profile: str) -> int: def get_id_by_username(self, profile: str) -> int:
"""Each Instagram profile has its own unique ID which stays unmodified even if a user changes """Each Instagram profile has its own unique ID which stays unmodified even if a user changes
his/her username. To get said ID, given the profile's name, you may call this function.""" his/her username. To get said ID, given the profile's name, you may call this function."""
return int(self.get_profile_metadata(profile)['user']['id']) return int(self.get_profile_metadata(profile)[0]['user']['id'])
def graphql_node_list(self, query_identifier: Union[int, str], query_variables: Dict[str, Any], def graphql_node_list(self, query_identifier: Union[int, str], query_variables: Dict[str, Any],
query_referer: Optional[str], query_referer: Optional[str],
edge_extractor: Callable[[Dict[str, Any]], Dict[str, Any]]) -> Iterator[Dict[str, Any]]: edge_extractor: Callable[[Dict[str, Any]], Dict[str, Any]],
rhx_gis: Optional[str] = None) -> Iterator[Dict[str, Any]]:
"""Retrieve a list of GraphQL nodes.""" """Retrieve a list of GraphQL nodes."""
query_variables['first'] = Instaloader.GRAPHQL_PAGE_LENGTH query_variables['first'] = Instaloader.GRAPHQL_PAGE_LENGTH
data = self.graphql_query(query_identifier, query_variables, query_referer) data = self.graphql_query(query_identifier, query_variables, query_referer, rhx_gis)
while True: while True:
edge_struct = edge_extractor(data) edge_struct = edge_extractor(data)
yield from [edge['node'] for edge in edge_struct['edges']] yield from [edge['node'] for edge in edge_struct['edges']]
if edge_struct['page_info']['has_next_page']: if edge_struct['page_info']['has_next_page']:
query_variables['after'] = edge_struct['page_info']['end_cursor'] query_variables['after'] = edge_struct['page_info']['end_cursor']
data = self.graphql_query(query_identifier, query_variables, query_referer) data = self.graphql_query(query_identifier, query_variables, query_referer, rhx_gis)
else: else:
break break
@@ -763,6 +783,7 @@ class Instaloader:
:param profile: Name of profile to lookup followers. :param profile: Name of profile to lookup followers.
""" """
self._log('(Warning: Fetching followers uses many queries, which are rate limited.)')
yield from self.graphql_node_list(17851374694183129, {'id': str(self.get_id_by_username(profile))}, yield from self.graphql_node_list(17851374694183129, {'id': str(self.get_id_by_username(profile))},
'https://www.instagram.com/' + profile + '/', 'https://www.instagram.com/' + profile + '/',
lambda d: d['data']['user']['edge_followed_by']) lambda d: d['data']['user']['edge_followed_by'])
@@ -775,6 +796,7 @@ class Instaloader:
:param profile: Name of profile to lookup followers. :param profile: Name of profile to lookup followers.
""" """
self._log('(Warning: Fetching followees uses many queries, which are rate limited.)')
yield from self.graphql_node_list(17874545323001329, {'id': str(self.get_id_by_username(profile))}, yield from self.graphql_node_list(17874545323001329, {'id': str(self.get_id_by_username(profile))},
'https://www.instagram.com/' + profile + '/', 'https://www.instagram.com/' + profile + '/',
lambda d: d['data']['user']['edge_follow']) lambda d: d['data']['user']['edge_follow'])
@@ -1141,7 +1163,7 @@ class Instaloader:
self._log("Retrieving stories from profile {}.".format(name)) self._log("Retrieving stories from profile {}.".format(name))
totalcount = len(user_stories["items"]) totalcount = len(user_stories["items"])
count = 1 count = 1
for item in user_stories["items"]: for item in reversed(user_stories["items"]):
self._log("[%3i/%3i] " % (count, totalcount), end="", flush=True) self._log("[%3i/%3i] " % (count, totalcount), end="", flush=True)
count += 1 count += 1
with self._error_catcher('Download story from user {}'.format(name)): with self._error_catcher('Download story from user {}'.format(name)):
@@ -1255,7 +1277,7 @@ class Instaloader:
if not self.is_logged_in: if not self.is_logged_in:
return return
data = self.get_profile_metadata(self.username) data, full_metadata = self.get_profile_metadata(self.username)
user_id = data["user"]["id"] user_id = data["user"]["id"]
while True: while True:
@@ -1275,7 +1297,8 @@ class Instaloader:
break break
data = self.graphql_query("f883d95537fbcd400f466f63d42bd8a1", data = self.graphql_query("f883d95537fbcd400f466f63d42bd8a1",
{'id': user_id, 'first': Instaloader.GRAPHQL_PAGE_LENGTH, {'id': user_id, 'first': Instaloader.GRAPHQL_PAGE_LENGTH,
'after': saved_media["page_info"]["end_cursor"]})['data'] 'after': saved_media["page_info"]["end_cursor"]},
rhx_gis=full_metadata['rhx_gis'])['data']
def download_saved_posts(self, max_count: int = None, fast_update: bool = False, def download_saved_posts(self, max_count: int = None, fast_update: bool = False,
filter_func: Optional[Callable[[Post], bool]] = None) -> None: filter_func: Optional[Callable[[Post], bool]] = None) -> None:
@@ -1302,10 +1325,19 @@ class Instaloader:
def get_hashtag_posts(self, hashtag: str) -> Iterator[Post]: def get_hashtag_posts(self, hashtag: str) -> Iterator[Post]:
"""Get Posts associated with a #hashtag.""" """Get Posts associated with a #hashtag."""
yield from (Post(self, node) for node in has_next_page = True
self.graphql_node_list(17875800862117404, {'tag_name': hashtag}, end_cursor = None
'https://www.instagram.com/explore/tags/{0}/'.format(hashtag), while has_next_page:
lambda d: d['data']['hashtag']['edge_hashtag_to_media'])) if end_cursor:
params = {'__a': 1, 'max_id': end_cursor}
else:
params = {'__a': 1}
hashtag_data = self.get_json('explore/tags/{0}/'.format(hashtag),
params)['graphql']['hashtag']['edge_hashtag_to_media']
yield from (Post(self, edge['node']) for edge in
hashtag_data['edges'])
has_next_page = hashtag_data['page_info']['has_next_page']
end_cursor = hashtag_data['page_info']['end_cursor']
def download_hashtag(self, hashtag: str, def download_hashtag(self, hashtag: str,
max_count: Optional[int] = None, max_count: Optional[int] = None,
@@ -1364,6 +1396,9 @@ class Instaloader:
self._log("Profile {0} does not match the stored unique ID {1}.".format(profile, profile_id)) self._log("Profile {0} does not match the stored unique ID {1}.".format(profile, profile_id))
else: else:
self._log("Trying to find profile {0} using its unique ID {1}.".format(profile, profile_id)) self._log("Trying to find profile {0} using its unique ID {1}.".format(profile, profile_id))
if not self.is_logged_in:
self.error("Profile {} changed its name. If you use --login=USERNAME, I can find out the new name.")
raise LoginRequiredException("Login required to obtain username from userid")
newname = self.get_username_by_id(profile_id) newname = self.get_username_by_id(profile_id)
self._log("Profile {0} has changed its name to {1}.".format(profile, newname)) self._log("Profile {0} has changed its name to {1}.".format(profile, newname))
if ((format_string_contains_key(self.dirname_pattern, 'profile') or if ((format_string_contains_key(self.dirname_pattern, 'profile') or
@@ -1389,15 +1424,15 @@ class Instaloader:
return profile, profile_id return profile, profile_id
raise ProfileNotExistsException("Profile {0} does not exist.".format(profile)) raise ProfileNotExistsException("Profile {0} does not exist.".format(profile))
def get_profile_metadata(self, profile_name: str) -> Dict[str, Any]: def get_profile_metadata(self, profile_name: str) -> Tuple[Dict[str, Any], Dict[str, Any]]:
"""Retrieves a profile's metadata, for use with e.g. :meth:`get_profile_posts` and :meth:`check_profile_id`.""" """Retrieves a profile's metadata, for use with e.g. :meth:`get_profile_posts` and :meth:`check_profile_id`."""
try: try:
metadata = self.get_json('{}/'.format(profile_name), params={'__a': 1}) metadata = self.get_json('{}/'.format(profile_name), params={})
return metadata['graphql'] if 'graphql' in metadata else metadata return metadata['entry_data']['ProfilePage'][0]['graphql'], metadata
except QueryReturnedNotFoundException: except QueryReturnedNotFoundException:
raise ProfileNotExistsException('Profile {} does not exist.'.format(profile_name)) raise ProfileNotExistsException('Profile {} does not exist.'.format(profile_name))
def get_profile_posts(self, profile_metadata: Dict[str, Any]) -> Iterator[Post]: def get_profile_posts(self, profile_metadata: Dict[str, Any], rhx_gis: str) -> Iterator[Post]:
"""Retrieve all posts from a profile.""" """Retrieve all posts from a profile."""
profile_name = profile_metadata['user']['username'] profile_name = profile_metadata['user']['username']
profile_id = int(profile_metadata['user']['id']) profile_id = int(profile_metadata['user']['id'])
@@ -1418,7 +1453,7 @@ class Instaloader:
data = self.graphql_query(17888483320059182, {'id': profile_metadata['user']['id'], data = self.graphql_query(17888483320059182, {'id': profile_metadata['user']['id'],
'first': Instaloader.GRAPHQL_PAGE_LENGTH, 'first': Instaloader.GRAPHQL_PAGE_LENGTH,
'after': end_cursor}, 'after': end_cursor},
'https://www.instagram.com/{0}/'.format(profile_name)) referer='https://www.instagram.com/{0}/'.format(profile_name), rhx_gis=rhx_gis)
media = data['data']['user']['edge_owner_to_timeline_media'] media = data['data']['user']['edge_owner_to_timeline_media']
yield from (Post(self, edge['node'], profile=profile_name, profile_id=profile_id) yield from (Post(self, edge['node'], profile=profile_name, profile_id=profile_id)
for edge in media['edges']) for edge in media['edges'])
@@ -1438,14 +1473,14 @@ class Instaloader:
with suppress(ProfileNotExistsException): with suppress(ProfileNotExistsException):
# ProfileNotExistsException is raised again later in check_profile_id() when we search the profile, so we # ProfileNotExistsException is raised again later in check_profile_id() when we search the profile, so we
# must suppress it here. # must suppress it here.
profile_metadata = self.get_profile_metadata(name) profile_metadata, full_metadata = self.get_profile_metadata(name)
# check if profile does exist or name has changed since last download # check if profile does exist or name has changed since last download
# and update name and json data if necessary # and update name and json data if necessary
name_updated, profile_id = self.check_profile_id(name, profile_metadata) name_updated, profile_id = self.check_profile_id(name, profile_metadata)
if name_updated != name: if name_updated != name:
name = name_updated name = name_updated
profile_metadata = self.get_profile_metadata(name) profile_metadata, full_metadata = self.get_profile_metadata(name)
# Download profile picture # Download profile picture
if profile_pic or profile_pic_only: if profile_pic or profile_pic_only:
@@ -1480,7 +1515,7 @@ class Instaloader:
else: else:
totalcount = profile_metadata["user"]["edge_owner_to_timeline_media"]["count"] totalcount = profile_metadata["user"]["edge_owner_to_timeline_media"]["count"]
count = 1 count = 1
for post in self.get_profile_posts(profile_metadata): for post in self.get_profile_posts(profile_metadata, rhx_gis=full_metadata['rhx_gis']):
self._log("[%3i/%3i] " % (count, totalcount), end="", flush=True) self._log("[%3i/%3i] " % (count, totalcount), end="", flush=True)
count += 1 count += 1
if filter_func is not None and not filter_func(post): if filter_func is not None and not filter_func(post):