diff --git a/docs/_static/style.css b/docs/_static/style.css
new file mode 100644
index 0000000..f34d322
--- /dev/null
+++ b/docs/_static/style.css
@@ -0,0 +1,3 @@
+code {
+ color: #222;
+}
\ No newline at end of file
diff --git a/docs/_templates/caption.html b/docs/_templates/caption.html
deleted file mode 100644
index a4527b5..0000000
--- a/docs/_templates/caption.html
+++ /dev/null
@@ -1,2 +0,0 @@
-
-Download pictures (or videos) along with their captions and other metadata from Instagram.
diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html
index 1ce3046..e722476 100644
--- a/docs/_templates/layout.html
+++ b/docs/_templates/layout.html
@@ -1,5 +1,11 @@
{% extends "!layout.html" %}
{% block extrahead %}
-
-
+{{ super() }}
+
+ {% if pagename == "index" %}
+
+ {% else %}
+
+ {% endif %}
+
{% endblock %}
diff --git a/docs/_templates/links.html b/docs/_templates/links.html
deleted file mode 100644
index b06cba1..0000000
--- a/docs/_templates/links.html
+++ /dev/null
@@ -1,23 +0,0 @@
-{% if next %}
-Next
-
-{% endif %}
-Current Release
-
-Links
-
-
diff --git a/docs/_templates/navbar.html b/docs/_templates/navbar.html
new file mode 100644
index 0000000..ecb9852
--- /dev/null
+++ b/docs/_templates/navbar.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+ {% if theme_navbar_links %}
+ {%- for link in theme_navbar_links %}
+ - {{ link[0] }}
+ {%- endfor %}
+ {% endif %}
+ {% block navbartoc %}
+ {% include "globaltoc.html" %}
+ {% if theme_navbar_pagenav %}
+ {% include "navbartoc.html" %}
+ {% endif %}
+ {% endblock %}
+ {% if theme_navbar_sidebarrel %}
+ {% block sidebarrel %}
+ {% include "relations.html" %}
+ {% endblock %}
+ {% endif %}
+
+
+
+
+
diff --git a/docs/_templates/rtdmessage.html b/docs/_templates/rtdmessage.html
deleted file mode 100644
index 6287a02..0000000
--- a/docs/_templates/rtdmessage.html
+++ /dev/null
@@ -1,3 +0,0 @@
-New Documentation URL
-
\ No newline at end of file
diff --git a/docs/conf.py b/docs/conf.py
index 560d504..db2e25a 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -20,6 +20,9 @@
import os
import subprocess
import sys
+
+import sphinx_bootstrap_theme
+
sys.path.insert(0, os.path.abspath('..'))
# -- General configuration ------------------------------------------------
@@ -133,16 +136,20 @@ todo_include_todos = False
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
-html_theme = 'alabaster'
+html_theme = 'bootstrap'
+html_theme_path = sphinx_bootstrap_theme.get_html_theme_path()
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {
- 'show_powered_by': False,
- 'sidebar_width': '290px',
- 'page_width': '935px' }
+ 'navbar_site_name': 'Site Contents',
+ 'navbar_pagenav_name': 'Page Contents',
+ 'navbar_pagenav': True,
+ 'navbar_sidebarrel': True,
+ 'nosidebar': True,
+ }
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
@@ -191,10 +198,8 @@ html_static_path = ['_static']
# Custom sidebar templates, maps document names to template names.
#
-if not os.environ.get("READTHEDOCS"):
- html_sidebars = {'**': ["caption.html", "globaltoc.html", "relations.html", "links.html"] }
-else:
- html_sidebars = {'**': ["caption.html", "rtdmessage.html", "globaltoc.html", "relations.html", "links.html"] }
+#html_sidebars = {'**': ["relations.html", "links.html"] }
+html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
@@ -373,3 +378,4 @@ def skip(app, what, name, obj, skip, options):
def setup(app):
app.connect('autodoc-skip-member', skip)
+ app.add_stylesheet("style.css")
diff --git a/docs/installation.rst b/docs/installation.rst
index 4c14c4f..2c5c54e 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -28,7 +28,7 @@ will be installed automatically, if it is not already installed.
- If you do not want to use pip, even though it is highly recommended,
and prefer to **install Instaloader manually**,
- `Download the Source `__,
+ `Download the Source `__,
extract the Zip or Tarball and execute ``instaloader.py`` from there.
- On **Arch Linux**, you may install Instaloader using the
@@ -36,3 +36,6 @@ will be installed automatically, if it is not already installed.
- On **Gentoo Linux**, you may install Instaloader using the
`Instaloader Ebuild `__.
+
+- On **Windows 10**, you may download the standalone executable from the
+ `current release page `__.
diff --git a/docs/requirements.txt b/docs/requirements.txt
index 914f619..2827417 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -1,3 +1,4 @@
requests
sphinx
sphinx-autodoc-typehints
+sphinx-bootstrap-theme
diff --git a/instaloader.py b/instaloader.py
index 48389f3..f50365d 100755
--- a/instaloader.py
+++ b/instaloader.py
@@ -31,14 +31,14 @@ import requests.utils
import urllib3
-__version__ = '3.3.3'
+__version__ = '3.3.4'
# NOTE: duplicated in README.rst and docs/index.rst
USAGE_STRING = """
{0} [--comments] [--geotags] [--stories]
- [--login YOUR-USERNAME] [--fast-update]
- profile | "#hashtag" | :stories | :feed | :saved
-{0} --help""".format(sys.argv[0])
+{2:{1}} [--login YOUR-USERNAME] [--fast-update]
+{2:{1}} profile | "#hashtag" | :stories | :feed | :saved
+{0} --help""".format(sys.argv[0], len(sys.argv[0]), '')
try:
# pylint:disable=wrong-import-position
@@ -413,7 +413,7 @@ class Post:
if loc_dict is not None:
location_json = self._instaloader.get_json("explore/locations/{0}/".format(loc_dict["id"]),
params={'__a': 1})
- return location_json["location"]
+ return location_json["location"] if "location" in location_json else location_json['graphql']['location']
@staticmethod
def json_encoder(obj) -> Dict[str, Any]:
@@ -525,10 +525,17 @@ class Profile:
def requested_by_viewer(self) -> bool:
return self._metadata['user']['requested_by_viewer']
- @property
- def profile_pic_url(self) -> str:
- return self._metadata["user"]["profile_pic_url_hd"] if "profile_pic_url_hd" in self._metadata["user"] \
- else self._metadata["user"]["profile_pic_url"]
+ def get_profile_pic_url(self) -> str:
+ """Return URL of profile picture"""
+ try:
+ with self._instaloader.get_anonymous_session() as anonymous_session:
+ data = self._instaloader.get_json(path='api/v1/users/{0}/info/'.format(self.userid), params={},
+ host='i.instagram.com', session=anonymous_session)
+ return data["user"]["hd_profile_pic_url_info"]["url"]
+ except (InstaloaderException, KeyError) as err:
+ self._instaloader.error('{} Unable to fetch high quality profile pic.'.format(err))
+ return self._metadata["user"]["profile_pic_url_hd"] if "profile_pic_url_hd" in self._metadata["user"] \
+ else self._metadata["user"]["profile_pic_url"]
def get_posts(self) -> Iterator[Post]:
"""Retrieve all posts from a profile."""
@@ -641,7 +648,7 @@ class Instaloader:
# configuration parameters
self.user_agent = user_agent if user_agent is not None else default_user_agent()
- self.session = self._get_anonymous_session()
+ self.session = self.get_anonymous_session()
self.username = None
self.sleep = sleep
self.quiet = quiet
@@ -736,7 +743,7 @@ class Instaloader:
:raises QueryReturnedForbiddenException: When the server responds with a 403.
:raises ConnectionException: When download repeatedly failed."""
try:
- with self._get_anonymous_session() as anonymous_session:
+ with self.get_anonymous_session() as anonymous_session:
resp = anonymous_session.get(url)
if resp.status_code == 200:
self._log(filename, end=' ', flush=True)
@@ -763,12 +770,13 @@ class Instaloader:
self.error("[skipped by user]", repeat_at_end=False)
raise ConnectionException(error_string)
- def get_json(self, url: str, params: Dict[str, Any],
+ def get_json(self, path: str, params: Dict[str, Any], host: str = 'www.instagram.com',
session: Optional[requests.Session] = None, _attempt = 1) -> Dict[str, Any]:
"""JSON request to Instagram.
- :param url: URL, relative to www.instagram.com/
+ :param path: URL, relative to the given domain which defaults to www.instagram.com/
:param params: GET parameters
+ :param host: Domain part of the URL from where to download the requested JSON; defaults to www.instagram.com
:param session: Session to use, or None to use self.session
:return: Decoded response dictionary
:raises QueryReturnedNotFoundException: When the server responds with a 404.
@@ -785,7 +793,7 @@ class Instaloader:
if len(timestamps) < 100 and not untracked_queries:
return 0
return round(min(timestamps) + sliding_window - current_time) + 6
- is_graphql_query = 'query_hash' in params and 'graphql/query' in url
+ is_graphql_query = 'query_hash' in params and 'graphql/query' in path
if is_graphql_query:
query_hash = params['query_hash']
waittime = graphql_query_waittime(query_hash)
@@ -800,11 +808,11 @@ class Instaloader:
sess = session if session else self.session
try:
self._sleep()
- resp = sess.get('https://www.instagram.com/' + url, params=params, allow_redirects=False)
+ resp = sess.get('https://{0}/{1}'.format(host, path), params=params, allow_redirects=False)
while resp.is_redirect:
redirect_url = resp.headers['location']
- self._log('\nHTTP redirect from {} to {}'.format('https://www.instagram.com/' + url, redirect_url))
- if redirect_url.index('https://www.instagram.com/') == 0:
+ self._log('\nHTTP redirect from https://{0}/{1} to {2}'.format(host, path, redirect_url))
+ if redirect_url.index('https://{}/'.format(host)) == 0:
resp = sess.get(redirect_url if redirect_url.endswith('/') else redirect_url + '/',
params=params, allow_redirects=False)
else:
@@ -824,7 +832,7 @@ class Instaloader:
raise ConnectionException("Returned \"{}\" status.".format(resp_json['status']))
return resp_json
except (ConnectionException, json.decoder.JSONDecodeError, requests.exceptions.RequestException) as err:
- error_string = "JSON Query to {}: {}".format(url, err)
+ error_string = "JSON Query to {}: {}".format(path, err)
if _attempt == self.max_connection_attempts:
raise ConnectionException(error_string)
self.error(error_string + " [retrying; skip with ^C]", repeat_at_end=False)
@@ -840,7 +848,7 @@ class Instaloader:
self._log('The request will be retried in {} seconds.'.format(waittime))
time.sleep(waittime)
self._sleep()
- return self.get_json(url, params, sess, _attempt + 1)
+ return self.get_json(path=path, params=params, host=host, session=sess, _attempt=_attempt + 1)
except KeyboardInterrupt:
self.error("[skipped by user]", repeat_at_end=False)
raise ConnectionException(error_string)
@@ -865,7 +873,7 @@ class Instaloader:
del header['X-Requested-With']
return header
- def _get_anonymous_session(self) -> requests.Session:
+ def get_anonymous_session(self) -> requests.Session:
"""Returns our default anonymous requests.Session object."""
session = requests.Session()
session.cookies.update({'sessionid': '', 'mid': '', 'ig_pr': '1',
@@ -1091,27 +1099,22 @@ class Instaloader:
def _epoch_to_string(epoch: datetime) -> str:
return epoch.strftime('%Y-%m-%d_%H-%M-%S')
- with self._get_anonymous_session() as anonymous_session:
- date_object = datetime.strptime(anonymous_session.head(profile.profile_pic_url).headers["Last-Modified"],
+ profile_pic_url = profile.get_profile_pic_url()
+ with self.get_anonymous_session() as anonymous_session:
+ date_object = datetime.strptime(anonymous_session.head(profile_pic_url).headers["Last-Modified"],
'%a, %d %b %Y %H:%M:%S GMT')
if ((format_string_contains_key(self.dirname_pattern, 'profile') or
format_string_contains_key(self.dirname_pattern, 'target'))):
filename = '{0}/{1}_UTC_profile_pic.{2}'.format(self.dirname_pattern.format(profile=profile.username.lower(),
target=profile.username.lower()),
- _epoch_to_string(date_object), profile.profile_pic_url[-3:])
+ _epoch_to_string(date_object), profile_pic_url[-3:])
else:
filename = '{0}/{1}_{2}_UTC_profile_pic.{3}'.format(self.dirname_pattern.format(), profile.username.lower(),
- _epoch_to_string(date_object), profile.profile_pic_url[-3:])
+ _epoch_to_string(date_object), profile_pic_url[-3:])
if os.path.isfile(filename):
self._log(filename + ' already exists')
return None
- url_best = re.sub(r'/s([1-9][0-9]{2})x\1/', '/s2048x2048/', profile.profile_pic_url)
- url_best = re.sub(r'/vp/[a-f0-9]{32}/[A-F0-9]{8}/', '/', url_best) # remove signature
- try:
- self._get_and_write_raw(url_best, filename)
- except (QueryReturnedForbiddenException, QueryReturnedNotFoundException) as err:
- self.error('{} Retrying with lower quality version.'.format(err))
- self._get_and_write_raw(profile.profile_pic_url, filename)
+ self._get_and_write_raw(profile_pic_url, filename)
os.utime(filename, (datetime.now().timestamp(), date_object.timestamp()))
self._log('') # log output of _get_and_write_raw() does not produce \n