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 @@ -

Instaloader

-

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 @@ + 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

-

Instaloader's Documentation has been moved to - https://instaloader.github.io.

\ 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