Compare commits

..
7 Commits
8 changed files with 209 additions and 81 deletions
+34
View File
@@ -314,6 +314,40 @@ when they were downloaded::
instaloader --post-metadata-txt="{likes} likes, {comments} comments." <target>/*.json.xz instaloader --post-metadata-txt="{likes} likes, {comments} comments." <target>/*.json.xz
.. _exit_codes:
Exit codes
^^^^^^^^^^
Different exit codes are used to indicate different kinds of error:
0
No error, all downloads were successful.
1
A non-fatal error happened. One or more posts, or even one or more
profiles could not be downloaded, but execution was not stopped. The
errors are repeated at the end of the log for easy access.
2
Command-line error. An unrecognized option was passed, or an invalid
combination of options, for example. No interaction with Instagram
was made.
3
Login error. It was not possible to login. Downloads were not
attempted.
4
Fatal download error. Downloads were interrupted and no further
attempts were made. Happens when a response with one of the status
codes in the :option:`--abort-on` option were passed, or when
Instagram logs the user out during downloads.
5
Interrupted by the user. Happens when the user presses Control-C or
sends SIGINT to the process.
.. _instaloader-as-cronjob: .. _instaloader-as-cronjob:
Instaloader as Cronjob Instaloader as Cronjob
+1 -1
View File
@@ -206,7 +206,7 @@ Instead of :option:`--login`, it is possible to use
This feature requires the browser_cookie3 library. This feature requires the browser_cookie3 library.
Compatible with :option:`--cookiefile` if you want to load cookies from browser profiles. Compatible with :option:`--cookiefile` if you want to load cookies from browser profiles.
Incompatible with :option:`--login` due to potential username mismatch between user input and browser login. Incompatible with :option:`--login` due to potential username mismatch between user input and browser login.
Supported browsers: Chrome, Firefox, Edge, Opera, Safari, Brave. Supported browsers: Brave, Chrome, Chromium, Edge, Firefox, LibreWolf, Opera, Opera_GX, Safari and Vivaldi.
In subsequent runs, you can just use :option:`--login` to reuse the In subsequent runs, you can just use :option:`--login` to reuse the
same session, which is saved by Instaloader. same session, which is saved by Instaloader.
+10
View File
@@ -27,16 +27,26 @@ Exceptions
.. autoexception:: LoginRequiredException .. autoexception:: LoginRequiredException
.. autoexception:: LoginException
.. versionadded:: 4.12
.. autoexception:: TwoFactorAuthRequiredException .. autoexception:: TwoFactorAuthRequiredException
.. versionadded:: 4.2 .. versionadded:: 4.2
.. versionchanged:: 4.12
Inherits LoginException
.. autoexception:: InvalidArgumentException .. autoexception:: InvalidArgumentException
.. autoexception:: BadResponseException .. autoexception:: BadResponseException
.. autoexception:: BadCredentialsException .. autoexception:: BadCredentialsException
.. versionchanged:: 4.12
Inherits LoginException
.. autoexception:: PostChangedException .. autoexception:: PostChangedException
.. autoexception:: QueryReturnedNotFoundException .. autoexception:: QueryReturnedNotFoundException
+1 -1
View File
@@ -1,7 +1,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."""
__version__ = '4.11.1' __version__ = '4.12'
try: try:
+74 -40
View File
@@ -6,10 +6,11 @@ import os
import re import re
import sys import sys
from argparse import ArgumentParser, ArgumentTypeError, SUPPRESS from argparse import ArgumentParser, ArgumentTypeError, SUPPRESS
from enum import IntEnum
from typing import List, Optional from typing import List, Optional
from . import (AbortDownloadException, BadCredentialsException, Instaloader, InstaloaderException, from . import (AbortDownloadException, BadCredentialsException, Instaloader, InstaloaderException,
InvalidArgumentException, Post, Profile, ProfileNotExistsException, StoryItem, InvalidArgumentException, LoginException, Post, Profile, ProfileNotExistsException, StoryItem,
TwoFactorAuthRequiredException, __version__, load_structure_from_file) TwoFactorAuthRequiredException, __version__, load_structure_from_file)
from .instaloader import (get_default_session_filename, get_default_stamps_filename) from .instaloader import (get_default_session_filename, get_default_stamps_filename)
from .instaloadercontext import default_user_agent from .instaloadercontext import default_user_agent
@@ -21,6 +22,15 @@ except ImportError:
bc3_library = False bc3_library = False
class ExitCode(IntEnum):
SUCCESS = 0
NON_FATAL_ERROR = 1
INIT_FAILURE = 2
LOGIN_FAILURE = 3
DOWNLOAD_ABORTED = 4
USER_ABORTED = 5
UNEXPECTED_ERROR = 99
def usage_string(): def usage_string():
# NOTE: duplicated in README.rst and docs/index.rst # NOTE: duplicated in README.rst and docs/index.rst
argv0 = os.path.basename(sys.argv[0]) argv0 = os.path.basename(sys.argv[0])
@@ -74,18 +84,22 @@ def filterstr_to_filterfunc(filter_str: str, item_type: type):
def get_cookies_from_instagram(domain, browser, cookie_file='', cookie_name=''): def get_cookies_from_instagram(domain, browser, cookie_file='', cookie_name=''):
supported_browsers = { supported_browsers = {
"chrome": browser_cookie3.chrome,
"firefox": browser_cookie3.firefox,
"edge": browser_cookie3.edge,
"brave": browser_cookie3.brave, "brave": browser_cookie3.brave,
"chrome": browser_cookie3.chrome,
"chromium": browser_cookie3.chromium,
"edge": browser_cookie3.edge,
"firefox": browser_cookie3.firefox,
"librewolf": browser_cookie3.librewolf,
"opera": browser_cookie3.opera, "opera": browser_cookie3.opera,
"safari": browser_cookie3.safari "opera_gx": browser_cookie3.opera_gx,
"safari": browser_cookie3.safari,
"vivaldi": browser_cookie3.vivaldi,
} }
if browser not in supported_browsers: if browser not in supported_browsers:
print("Loading cookies from the specified browser failed") raise InvalidArgumentException("Loading cookies from the specified browser failed\n"
print("Supported browsers are Chrome, Firefox, Edge, Brave, Opera and Safari") "Supported browsers are Brave, Chrome, Chromium, Edge, Firefox, LibreWolf, "
return {} "Opera, Opera_GX, Safari and Vivaldi")
cookies = {} cookies = {}
browser_cookies = list(supported_browsers[browser](cookie_file=cookie_file)) browser_cookies = list(supported_browsers[browser](cookie_file=cookie_file))
@@ -97,7 +111,8 @@ def get_cookies_from_instagram(domain, browser, cookie_file='', cookie_name=''):
if cookies: if cookies:
print(f"Cookies loaded successfully from {browser}") print(f"Cookies loaded successfully from {browser}")
else: else:
print(f"No cookies found for Instagram in {browser}, Are you logged in succesfully in {browser}?") raise LoginException(f"No cookies found for Instagram in {browser}, "
f"Are you logged in succesfully in {browser}?")
if cookie_name: if cookie_name:
return cookies.get(cookie_name, {}) return cookies.get(cookie_name, {})
@@ -111,7 +126,7 @@ def import_session(browser, instaloader, cookiefile):
instaloader.context.update_cookies(cookie) instaloader.context.update_cookies(cookie)
username = instaloader.test_login() username = instaloader.test_login()
if not username: if not username:
raise SystemExit(f"Not logged in. Are you logged in successfully in {browser}?") raise LoginException(f"Not logged in. Are you logged in successfully in {browser}?")
instaloader.context.username = username instaloader.context.username = username
print(f"{username} has been successfully logged in.") print(f"{username} has been successfully logged in.")
print(f"Next time use --login={username} to reuse the same session.") print(f"Next time use --login={username} to reuse the same session.")
@@ -130,7 +145,7 @@ def _main(instaloader: Instaloader, targetlist: List[str],
max_count: Optional[int] = None, post_filter_str: Optional[str] = None, max_count: Optional[int] = None, post_filter_str: Optional[str] = None,
storyitem_filter_str: Optional[str] = None, storyitem_filter_str: Optional[str] = None,
browser: Optional[str] = None, browser: Optional[str] = None,
cookiefile: Optional[str] = None) -> None: cookiefile: Optional[str] = None) -> ExitCode:
"""Download set of profiles, hashtags etc. and handle logging in and session files if desired.""" """Download set of profiles, hashtags etc. and handle logging in and session files if desired."""
# Parse and generate filter function # Parse and generate filter function
post_filter = None post_filter = None
@@ -149,7 +164,7 @@ def _main(instaloader: Instaloader, targetlist: List[str],
if browser and bc3_library: if browser and bc3_library:
import_session(browser.lower(), instaloader, cookiefile) import_session(browser.lower(), instaloader, cookiefile)
elif browser and not bc3_library: elif browser and not bc3_library:
raise SystemExit("browser_cookie3 library is needed to load cookies from browsers") raise InvalidArgumentException("browser_cookie3 library is needed to load cookies from browsers")
# Login, if desired # Login, if desired
if username is not None: if username is not None:
if not re.match(r"^[A-Za-z0-9._]+$", username): if not re.match(r"^[A-Za-z0-9._]+$", username):
@@ -178,7 +193,11 @@ def _main(instaloader: Instaloader, targetlist: List[str],
print(err, file=sys.stderr) print(err, file=sys.stderr)
pass pass
else: else:
instaloader.interactive_login(username) try:
instaloader.interactive_login(username)
except KeyboardInterrupt:
print("\nInterrupted by user.", file=sys.stderr)
return ExitCode.USER_ABORTED
instaloader.context.log("Logged in as %s." % username) instaloader.context.log("Logged in as %s." % username)
# since 4.2.9 login is required for geotags # since 4.2.9 login is required for geotags
if instaloader.download_geotags and not instaloader.context.is_logged_in: if instaloader.download_geotags and not instaloader.context.is_logged_in:
@@ -186,6 +205,7 @@ def _main(instaloader: Instaloader, targetlist: List[str],
# Try block for KeyboardInterrupt (save session on ^C) # Try block for KeyboardInterrupt (save session on ^C)
profiles = set() profiles = set()
anonymous_retry_profiles = set() anonymous_retry_profiles = set()
exit_code = ExitCode.SUCCESS
try: try:
# Generate set of profiles, already downloading non-profile targets # Generate set of profiles, already downloading non-profile targets
for target in targetlist: for target in targetlist:
@@ -291,8 +311,10 @@ def _main(instaloader: Instaloader, targetlist: List[str],
latest_stamps=latest_stamps) latest_stamps=latest_stamps)
except KeyboardInterrupt: except KeyboardInterrupt:
print("\nInterrupted by user.", file=sys.stderr) print("\nInterrupted by user.", file=sys.stderr)
exit_code = ExitCode.USER_ABORTED
except AbortDownloadException as exc: except AbortDownloadException as exc:
print("\nDownload aborted: {}.".format(exc), file=sys.stderr) print("\nDownload aborted: {}.".format(exc), file=sys.stderr)
exit_code = ExitCode.DOWNLOAD_ABORTED
# Save session if it is useful # Save session if it is useful
if instaloader.context.is_logged_in: if instaloader.context.is_logged_in:
instaloader.save_session_to_file(sessionfile) instaloader.save_session_to_file(sessionfile)
@@ -304,6 +326,8 @@ def _main(instaloader: Instaloader, targetlist: List[str],
else: else:
# Instaloader did not do anything # Instaloader did not do anything
instaloader.context.log("usage:" + usage_string()) instaloader.context.log("usage:" + usage_string())
exit_code = ExitCode.INIT_FAILURE
return exit_code
def main(): def main():
@@ -486,11 +510,11 @@ def main():
print("Login is required to download stories.", file=sys.stderr) print("Login is required to download stories.", file=sys.stderr)
args.stories = False args.stories = False
if args.stories_only: if args.stories_only:
raise SystemExit(1) raise InvalidArgumentException()
if ':feed-all' in args.profile or ':feed-liked' in args.profile: if ':feed-all' in args.profile or ':feed-liked' in args.profile:
raise SystemExit(":feed-all and :feed-liked were removed. Use :feed as target and " raise InvalidArgumentException(":feed-all and :feed-liked were removed. Use :feed as target and "
"eventually --post-filter=viewer_has_liked.") "eventually --post-filter=viewer_has_liked.")
post_metadata_txt_pattern = '\n'.join(args.post_metadata_txt) if args.post_metadata_txt else None post_metadata_txt_pattern = '\n'.join(args.post_metadata_txt) if args.post_metadata_txt else None
storyitem_metadata_txt_pattern = '\n'.join(args.storyitem_metadata_txt) if args.storyitem_metadata_txt else None storyitem_metadata_txt_pattern = '\n'.join(args.storyitem_metadata_txt) if args.storyitem_metadata_txt else None
@@ -500,18 +524,18 @@ def main():
post_metadata_txt_pattern = '' post_metadata_txt_pattern = ''
storyitem_metadata_txt_pattern = '' storyitem_metadata_txt_pattern = ''
else: else:
raise SystemExit("--no-captions and --post-metadata-txt or --storyitem-metadata-txt given; " raise InvalidArgumentException("--no-captions and --post-metadata-txt or --storyitem-metadata-txt "
"That contradicts.") "given; That contradicts.")
if args.no_resume and args.resume_prefix: if args.no_resume and args.resume_prefix:
raise SystemExit("--no-resume and --resume-prefix given; That contradicts.") raise InvalidArgumentException("--no-resume and --resume-prefix given; That contradicts.")
resume_prefix = (args.resume_prefix if args.resume_prefix else 'iterator') if not args.no_resume else None resume_prefix = (args.resume_prefix if args.resume_prefix else 'iterator') if not args.no_resume else None
if args.no_pictures and args.fast_update: if args.no_pictures and args.fast_update:
raise SystemExit('--no-pictures and --fast-update cannot be used together.') raise InvalidArgumentException('--no-pictures and --fast-update cannot be used together.')
if args.login and args.load_cookies: if args.login and args.load_cookies:
raise SystemExit('--load-cookies and --login cannot be used together.') raise InvalidArgumentException('--load-cookies and --login cannot be used together.')
# Determine what to download # Determine what to download
download_profile_pic = not args.no_profile_pic or args.profile_pic_only download_profile_pic = not args.no_profile_pic or args.profile_pic_only
@@ -536,27 +560,37 @@ def main():
iphone_support=not args.no_iphone, iphone_support=not args.no_iphone,
title_pattern=args.title_pattern, title_pattern=args.title_pattern,
sanitize_paths=args.sanitize_paths) sanitize_paths=args.sanitize_paths)
_main(loader, exit_code = _main(loader,
args.profile, args.profile,
username=args.login.lower() if args.login is not None else None, username=args.login.lower() if args.login is not None else None,
password=args.password, password=args.password,
sessionfile=args.sessionfile, sessionfile=args.sessionfile,
download_profile_pic=download_profile_pic, download_profile_pic=download_profile_pic,
download_posts=download_posts, download_posts=download_posts,
download_stories=download_stories, download_stories=download_stories,
download_highlights=args.highlights, download_highlights=args.highlights,
download_tagged=args.tagged, download_tagged=args.tagged,
download_igtv=args.igtv, download_igtv=args.igtv,
fast_update=args.fast_update, fast_update=args.fast_update,
latest_stamps_file=args.latest_stamps, latest_stamps_file=args.latest_stamps,
max_count=int(args.count) if args.count is not None else None, max_count=int(args.count) if args.count is not None else None,
post_filter_str=args.post_filter, post_filter_str=args.post_filter,
storyitem_filter_str=args.storyitem_filter, storyitem_filter_str=args.storyitem_filter,
browser=args.load_cookies, browser=args.load_cookies,
cookiefile=args.cookiefile) cookiefile=args.cookiefile)
loader.close() loader.close()
if loader.has_stored_errors:
exit_code = ExitCode.NON_FATAL_ERROR
except InvalidArgumentException as err:
print(err, file=sys.stderr)
exit_code = ExitCode.INIT_FAILURE
except LoginException as err:
print(err, file=sys.stderr)
exit_code = ExitCode.LOGIN_FAILURE
except InstaloaderException as err: except InstaloaderException as err:
raise SystemExit("Fatal error: %s" % err) from err print("Fatal error: %s" % err)
exit_code = ExitCode.UNEXPECTED_ERROR
sys.exit(exit_code)
if __name__ == "__main__": if __name__ == "__main__":
+6 -2
View File
@@ -33,7 +33,11 @@ class LoginRequiredException(InstaloaderException):
pass pass
class TwoFactorAuthRequiredException(InstaloaderException): class LoginException(InstaloaderException):
pass
class TwoFactorAuthRequiredException(LoginException):
pass pass
@@ -45,7 +49,7 @@ class BadResponseException(InstaloaderException):
pass pass
class BadCredentialsException(InstaloaderException): class BadCredentialsException(LoginException):
pass pass
+24 -7
View File
@@ -643,11 +643,16 @@ class Instaloader:
def login(self, user: str, passwd: str) -> None: def login(self, user: str, passwd: str) -> None:
"""Log in to instagram with given username and password and internally store session object. """Log in to instagram with given username and password and internally store session object.
:raises InvalidArgumentException: If the provided username does not exist.
:raises BadCredentialsException: If the provided password is wrong. :raises BadCredentialsException: If the provided password is wrong.
:raises ConnectionException: If connection to Instagram failed.
:raises TwoFactorAuthRequiredException: First step of 2FA login done, now call :raises TwoFactorAuthRequiredException: First step of 2FA login done, now call
:meth:`Instaloader.two_factor_login`.""" :meth:`Instaloader.two_factor_login`.
:raises LoginException: An error happened during login (for example, an invalid response was received).
Or if the provided username does not exist.
.. versionchanged:: 4.12
Raises LoginException instead of ConnectionException when an error happens.
Raises LoginException instead of InvalidArgumentException when the username does not exist.
"""
self.context.login(user, passwd) self.context.login(user, passwd)
def two_factor_login(self, two_factor_code) -> None: def two_factor_login(self, two_factor_code) -> None:
@@ -1582,11 +1587,16 @@ class Instaloader:
def interactive_login(self, username: str) -> None: def interactive_login(self, username: str) -> None:
"""Logs in and internally stores session, asking user for password interactively. """Logs in and internally stores session, asking user for password interactively.
:raises LoginRequiredException: when in quiet mode. :raises InvalidArgumentException: when in quiet mode.
:raises InvalidArgumentException: If the provided username does not exist. :raises LoginException: If the provided username does not exist.
:raises ConnectionException: If connection to Instagram failed.""" :raises ConnectionException: If connection to Instagram failed.
.. versionchanged:: 4.12
Raises InvalidArgumentException instead of LoginRequiredException when in quiet mode.
Raises LoginException instead of InvalidArgumentException when the username does not exist.
"""
if self.context.quiet: if self.context.quiet:
raise LoginRequiredException("Quiet mode requires given password or valid session file.") raise InvalidArgumentException("Quiet mode requires given password or valid session file.")
try: try:
password = None password = None
while password is None: while password is None:
@@ -1605,3 +1615,10 @@ class Instaloader:
except BadCredentialsException as err: except BadCredentialsException as err:
print(err, file=sys.stderr) print(err, file=sys.stderr)
pass pass
@property
def has_stored_errors(self) -> bool:
"""Returns whether any error has been reported and stored to be repeated at program termination.
.. versionadded: 4.12"""
return self.context.has_stored_errors
+59 -30
View File
@@ -9,7 +9,7 @@ import textwrap
import time import time
import urllib.parse import urllib.parse
import uuid import uuid
from contextlib import contextmanager from contextlib import contextmanager, suppress
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import partial from functools import partial
from typing import Any, Callable, Dict, Iterator, List, Optional, Union from typing import Any, Callable, Dict, Iterator, List, Optional, Union
@@ -151,6 +151,13 @@ class InstaloaderContext:
if repeat_at_end: if repeat_at_end:
self.error_log.append(msg) self.error_log.append(msg)
@property
def has_stored_errors(self) -> bool:
"""Returns whether any error has been reported and stored to be repeated at program termination.
.. versionadded: 4.12"""
return bool(self.error_log)
def close(self): def close(self):
"""Print error log and close session""" """Print error log and close session"""
if self.error_log and not self.quiet: if self.error_log and not self.quiet:
@@ -236,17 +243,26 @@ class InstaloaderContext:
def test_login(self) -> Optional[str]: def test_login(self) -> Optional[str]:
"""Not meant to be used directly, use :meth:`Instaloader.test_login`.""" """Not meant to be used directly, use :meth:`Instaloader.test_login`."""
data = self.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {}) try:
return data["data"]["user"]["username"] if data["data"]["user"] is not None else None data = self.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {})
return data["data"]["user"]["username"] if data["data"]["user"] is not None else None
except (AbortDownloadException, ConnectionException) as err:
self.error(f"Error when checking if logged in: {err}")
return None
def login(self, user, passwd): def login(self, user, passwd):
"""Not meant to be used directly, use :meth:`Instaloader.login`. """Not meant to be used directly, use :meth:`Instaloader.login`.
:raises InvalidArgumentException: If the provided username does not exist.
:raises BadCredentialsException: If the provided password is wrong. :raises BadCredentialsException: If the provided password is wrong.
:raises ConnectionException: If connection to Instagram failed.
:raises TwoFactorAuthRequiredException: First step of 2FA login done, now call :raises TwoFactorAuthRequiredException: First step of 2FA login done, now call
:meth:`Instaloader.two_factor_login`.""" :meth:`Instaloader.two_factor_login`.
:raises LoginException: An error happened during login (for example, and invalid response).
Or if the provided username does not exist.
.. versionchanged:: 4.12
Raises LoginException instead of ConnectionException when an error happens.
Raises LoginException instead of InvalidArgumentException when the username does not exist.
"""
# pylint:disable=import-outside-toplevel # pylint:disable=import-outside-toplevel
import http.client import http.client
# pylint:disable=protected-access # pylint:disable=protected-access
@@ -277,7 +293,7 @@ class InstaloaderContext:
resp_json = login.json() resp_json = login.json()
except json.decoder.JSONDecodeError as err: except json.decoder.JSONDecodeError as err:
raise ConnectionException( raise LoginException(
"Login error: JSON decode fail, {} - {}.".format(login.status_code, login.reason) "Login error: JSON decode fail, {} - {}.".format(login.status_code, login.reason)
) from err ) from err
if resp_json.get('two_factor_required'): if resp_json.get('two_factor_required'):
@@ -289,32 +305,32 @@ class InstaloaderContext:
resp_json['two_factor_info']['two_factor_identifier']) resp_json['two_factor_info']['two_factor_identifier'])
raise TwoFactorAuthRequiredException("Login error: two-factor authentication required.") raise TwoFactorAuthRequiredException("Login error: two-factor authentication required.")
if resp_json.get('checkpoint_url'): if resp_json.get('checkpoint_url'):
raise ConnectionException( raise LoginException(
f"Login: Checkpoint required. Point your browser to {resp_json.get('checkpoint_url')} - " f"Login: Checkpoint required. Point your browser to {resp_json.get('checkpoint_url')} - "
f"follow the instructions, then retry." f"follow the instructions, then retry."
) )
if resp_json['status'] != 'ok': if resp_json['status'] != 'ok':
if 'message' in resp_json: if 'message' in resp_json:
raise ConnectionException("Login error: \"{}\" status, message \"{}\".".format(resp_json['status'], raise LoginException("Login error: \"{}\" status, message \"{}\".".format(resp_json['status'],
resp_json['message'])) resp_json['message']))
else: else:
raise ConnectionException("Login error: \"{}\" status.".format(resp_json['status'])) raise LoginException("Login error: \"{}\" status.".format(resp_json['status']))
if 'authenticated' not in resp_json: if 'authenticated' not in resp_json:
# Issue #472 # Issue #472
if 'message' in resp_json: if 'message' in resp_json:
raise ConnectionException("Login error: Unexpected response, \"{}\".".format(resp_json['message'])) raise LoginException("Login error: Unexpected response, \"{}\".".format(resp_json['message']))
else: else:
raise ConnectionException("Login error: Unexpected response, this might indicate a blocked IP.") raise LoginException("Login error: Unexpected response, this might indicate a blocked IP.")
if not resp_json['authenticated']: if not resp_json['authenticated']:
if resp_json['user']: if resp_json['user']:
# '{"authenticated": false, "user": true, "status": "ok"}' # '{"authenticated": false, "user": true, "status": "ok"}'
raise BadCredentialsException('Login error: Wrong password.') raise BadCredentialsException('Login error: Wrong password.')
else: else:
# '{"authenticated": false, "user": false, "status": "ok"}' # '{"authenticated": false, "user": false, "status": "ok"}'
# Raise InvalidArgumentException rather than BadCredentialException, because BadCredentialException # Raise LoginException rather than BadCredentialException, because BadCredentialException
# triggers re-asking of password in Instaloader.interactive_login(), which makes no sense if the # triggers re-asking of password in Instaloader.interactive_login(), which makes no sense if the
# username is invalid. # username is invalid.
raise InvalidArgumentException('Login error: User {} does not exist.'.format(user)) raise LoginException('Login error: User {} does not exist.'.format(user))
# '{"authenticated": true, "user": true, "userId": ..., "oneTapPrompt": false, "status": "ok"}' # '{"authenticated": true, "user": true, "userId": ..., "oneTapPrompt": false, "status": "ok"}'
session.headers.update({'X-CSRFToken': login.cookies['csrftoken']}) session.headers.update({'X-CSRFToken': login.cookies['csrftoken']})
self._session = session self._session = session
@@ -352,6 +368,23 @@ class InstaloaderContext:
if self.sleep: if self.sleep:
time.sleep(min(random.expovariate(0.6), 15.0)) time.sleep(min(random.expovariate(0.6), 15.0))
@staticmethod
def _response_error(resp: requests.Response) -> str:
extra_from_json: Optional[str] = None
with suppress(json.decoder.JSONDecodeError):
resp_json = resp.json()
if "status" in resp_json:
extra_from_json = (
f"\"{resp_json['status']}\" status, message \"{resp_json['message']}\""
if "message" in resp_json
else f"\"{resp_json['status']}\" status"
)
return (
f"{resp.status_code} {resp.reason}"
f"{f' - {extra_from_json}' if extra_from_json is not None else ''}"
f" when accessing {resp.url}"
)
def get_json(self, path: str, params: Dict[str, Any], host: str = 'www.instagram.com', def get_json(self, path: str, params: Dict[str, Any], host: str = 'www.instagram.com',
session: Optional[requests.Session] = None, _attempt=1, session: Optional[requests.Session] = None, _attempt=1,
response_headers: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: response_headers: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
@@ -405,21 +438,17 @@ class InstaloaderContext:
response_headers.clear() response_headers.clear()
response_headers.update(resp.headers) response_headers.update(resp.headers)
if resp.status_code == 400: if resp.status_code == 400:
raise QueryReturnedBadRequestException("400 Bad Request") raise QueryReturnedBadRequestException(self._response_error(resp))
if resp.status_code == 404: if resp.status_code == 404:
raise QueryReturnedNotFoundException("404 Not Found") raise QueryReturnedNotFoundException(self._response_error(resp))
if resp.status_code == 429: if resp.status_code == 429:
raise TooManyRequestsException("429 Too Many Requests") raise TooManyRequestsException(self._response_error(resp))
if resp.status_code != 200: if resp.status_code != 200:
raise ConnectionException("HTTP error code {}.".format(resp.status_code)) raise ConnectionException(self._response_error(resp))
else: else:
resp_json = resp.json() 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: raise ConnectionException(self._response_error(resp))
raise ConnectionException("Returned \"{}\" status, message \"{}\".".format(resp_json['status'],
resp_json['message']))
else:
raise ConnectionException("Returned \"{}\" status.".format(resp_json['status']))
return resp_json return resp_json
except (ConnectionException, json.decoder.JSONDecodeError, requests.exceptions.RequestException) as err: except (ConnectionException, json.decoder.JSONDecodeError, requests.exceptions.RequestException) as err:
error_string = "JSON Query to {}: {}".format(path, err) error_string = "JSON Query to {}: {}".format(path, err)
@@ -608,11 +637,11 @@ class InstaloaderContext:
else: else:
if resp.status_code == 403: if resp.status_code == 403:
# suspected invalid URL signature # suspected invalid URL signature
raise QueryReturnedForbiddenException("403 when accessing {}.".format(url)) raise QueryReturnedForbiddenException(self._response_error(resp))
if resp.status_code == 404: if resp.status_code == 404:
# 404 not worth retrying. # 404 not worth retrying.
raise QueryReturnedNotFoundException("404 when accessing {}.".format(url)) raise QueryReturnedNotFoundException(self._response_error(resp))
raise ConnectionException("HTTP error code {}.".format(resp.status_code)) raise ConnectionException(self._response_error(resp))
def get_and_write_raw(self, url: str, filename: str) -> None: def get_and_write_raw(self, url: str, filename: str) -> None:
"""Downloads and writes anonymously-requested raw data into a file. """Downloads and writes anonymously-requested raw data into a file.
@@ -638,11 +667,11 @@ class InstaloaderContext:
else: else:
if resp.status_code == 403: if resp.status_code == 403:
# suspected invalid URL signature # suspected invalid URL signature
raise QueryReturnedForbiddenException("403 when accessing {}.".format(url)) raise QueryReturnedForbiddenException(self._response_error(resp))
if resp.status_code == 404: if resp.status_code == 404:
# 404 not worth retrying. # 404 not worth retrying.
raise QueryReturnedNotFoundException("404 when accessing {}.".format(url)) raise QueryReturnedNotFoundException(self._response_error(resp))
raise ConnectionException("HTTP error code {}.".format(resp.status_code)) raise ConnectionException(self._response_error(resp))
@property @property
def root_rhx_gis(self) -> Optional[str]: def root_rhx_gis(self) -> Optional[str]: