Compare commits

...
11 Commits
Author SHA1 Message Date
André Koch-Kramer d28bd3ab52 Release of version 2.2.3 2017-08-13 14:39:35 +02:00
Alexander Graf adeb588471 Fix KeyError when generating NodeUnavailable error
Fixes #32.
2017-08-13 12:51:39 +02:00
André Koch-Kramer a894c2d206 Wait to avoid error code 429 (Too Many Requests)
Additional sleeps are necessary because Instagram is rate limiting
GraphQL queries. The error does not occur if not more than 100 queries
are made in a sliding window of eleven minutes.
Fixes #29
2017-08-12 19:57:54 +02:00
André Koch-Kramer 86fb80d7e4 Avoid GraphQL queries if all comments in metadata
If get_node_metadata() is able to provide all comments of a node, no
additional query is needed. Especially GraphQL queries are time
expensive because no more than 100 can be queried in ten minutes. Since
get_node_metadata() does not use GraphQL queries, this is a usefull
tradeoff.
+ Additional error handling
2017-08-12 19:04:22 +02:00
Alexander Graf 9eddc03cf1 Don't rely on fromtimestamp() raising ValueError
Instead of relying on that datetime.fromtimestamp() raises ValueError if
the timestamp of a story is in milliseconds rather than seconds, we now
compare the timestamp value with a timestamp of the year 2286 to decide
whether to divide it by 1000 or not.

This is motivated by #30, where an OSError is raised in
datetime.fromtimestamp() under Windows.
2017-08-09 18:27:05 +02:00
André Koch-Kramer 42864997b3 get_node_metadata() now also catches TypeError
Concerns #26
2017-08-08 00:43:52 +02:00
Alexander Graf 5d83a4ccf6 Release of version 2.2.2 2017-08-06 10:49:36 +02:00
Alexander Graf 56c8ac729f Cleanup .gitignore 2017-08-05 13:43:26 +02:00
André Koch-Kramer 987d95c048 Forgot "file=" in some print statements 2017-08-05 00:22:43 +02:00
André Koch-Kramer 9b5d4e34fc Raise and catch NodeUnavailableException
In case a node can not be downloaded or its metadata is needed and can
not be retrieved, a NodeUnavailableException is raised and the according
node will be skipped.
Concerns #26
2017-08-04 19:19:56 +02:00
Alexander Graf 838ea645a8 Let Travis automatically deploy releases to PyPi 2017-08-04 11:18:20 +02:00
4 changed files with 153 additions and 53 deletions
-3
View File
@@ -1,4 +1 @@
__pycache__/ __pycache__/
build/
dist/
instaloader.egg-info/
+8
View File
@@ -10,3 +10,11 @@ install:
- pip install pylint requests - pip install pylint requests
script: script:
- python3 -m pylint -r n -d bad-whitespace,missing-docstring,too-many-arguments,locally-disabled,line-too-long,too-many-public-methods,too-many-lines,too-many-instance-attributes,too-many-locals,too-many-branches,too-many-statements instaloader - python3 -m pylint -r n -d bad-whitespace,missing-docstring,too-many-arguments,locally-disabled,line-too-long,too-many-public-methods,too-many-lines,too-many-instance-attributes,too-many-locals,too-many-branches,too-many-statements instaloader
deploy:
provider: pypi
user: aandergr
on:
tags: true
python: 3.6
password:
secure: KBsATtXNnEzqr0X2ALdyx2zaYj2EUfK8YjP4jEOilBELpJ4evHYXVgRkn9CKXGsD7LKbWmR3gz2rr8NZ6U42pHw0fpQHizz91MpJSKbNY7RZZh4UVWMDh81izeglchWAF+m3Wdpxpo/GBDbrZIbRQFhPgB+kjSzJo3uiCS+yVXtGG+6csJHzd0WkJjsVHz+XSdxyOiLy/qFF3nmY6ZrVrgw6WbX2YLbx9je6DxK3YRrnBqgMgH7Ui2xWWbgJCk3KKpQTMiMKnTa9mdeMJAd26YMCz1gG/X7ck6VNly/6bs5+Zq7/bU0dSolhPBhNthyccYdS0IeGPdPF3YqatblJ4fLGTfHn8oJs3/T11yIW9pvOT3UftEynPOUX+en2Qnb/3WfgwUfVts8i8DAYgEFLGEzbF5XMGy4gwAD/bZ0JW0pmgAG2PZsdOokAt+ydlNj/uxq1GFW2doZbul6k4t03p1mik6itYscvuhKoEm8Yw6wr1P9aDvMR6FFpSu4gDt2XrKmRmWkSXxpNeCfZujTjyAnISxLVQGSY7BG5K0Z2HbDtqqNz9nj4WgNGzZmngiRofCevWR+vPIDUDW7NCX03csfcFS9RRllBO/BrL7NVUV+Qp9dfTQYp+8SqYLWgHtDoXdgyMREjJaAmbx2u/TYTY8bADZ5F3vGUpv2j5hP3NTU=
+144 -49
View File
@@ -80,6 +80,10 @@ class BadResponseException(NonfatalException):
pass pass
class NodeUnavailableException(NonfatalException):
pass
class BadCredentialsException(InstaloaderException): class BadCredentialsException(InstaloaderException):
pass pass
@@ -145,6 +149,7 @@ class Instaloader:
self.dirname_pattern = dirname_pattern if dirname_pattern is not None else '{target}' self.dirname_pattern = dirname_pattern if dirname_pattern is not None else '{target}'
self.filename_pattern = filename_pattern.replace('{date}', '{date:%Y-%m-%d_%H-%M-%S}') \ self.filename_pattern = filename_pattern.replace('{date}', '{date:%Y-%m-%d_%H-%M-%S}') \
if filename_pattern is not None else '{date:%Y-%m-%d_%H-%M-%S}' if filename_pattern is not None else '{date:%Y-%m-%d_%H-%M-%S}'
self.previous_queries = dict()
def _log(self, *msg, sep='', end='\n', flush=False): def _log(self, *msg, sep='', end='\n', flush=False):
if not self.quiet: if not self.quiet:
@@ -166,9 +171,9 @@ class Instaloader:
else: else:
raise ConnectionException("Request returned HTTP error code {}.".format(resp.status_code)) raise ConnectionException("Request returned HTTP error code {}.".format(resp.status_code))
except (urllib3.exceptions.HTTPError, requests.exceptions.RequestException, ConnectionException) as err: except (urllib3.exceptions.HTTPError, requests.exceptions.RequestException, ConnectionException) as err:
print("URL: " + url + "\n" + err, file=sys.stderr) print("URL: {}\n{}".format(url, err), file=sys.stderr)
if tries <= 1: if tries <= 1:
raise err raise NodeUnavailableException
self._sleep() self._sleep()
self._get_and_write_raw(url, filename, tries - 1) self._get_and_write_raw(url, filename, tries - 1)
@@ -216,13 +221,14 @@ class Instaloader:
return session return session
def graphql_query(self, query_id: int, variables: Dict[str, Any], def graphql_query(self, query_id: int, variables: Dict[str, Any],
referer: Optional[str] = None) -> Dict[str, Any]: referer: Optional[str] = None, tries: int = 3) -> Dict[str, Any]:
""" """
Do a GraphQL Query. Do a GraphQL Query.
:param query_id: Query ID. :param query_id: Query ID.
:param variables: Variables for the Query. :param variables: Variables for the Query.
:param referer: HTTP Referer, or None. :param referer: HTTP Referer, or None.
:param tries: Number of total tries. Retries if a RequestException occurs.
:return: The server's response dictionary. :return: The server's response dictionary.
""" """
tmpsession = copy_session(self.session) tmpsession = copy_session(self.session)
@@ -234,13 +240,57 @@ class Instaloader:
tmpsession.headers['accept'] = '*/*' tmpsession.headers['accept'] = '*/*'
if referer is not None: if referer is not None:
tmpsession.headers['referer'] = referer tmpsession.headers['referer'] = referer
sliding_window = 660
def graphql_query_waittime(query_id: int) -> int:
timestamp_list = self.previous_queries.get(query_id)
if not timestamp_list:
return 0
current_time = datetime.now().timestamp()
timestamp_list = list(filter(lambda t: t > current_time - sliding_window, timestamp_list))
self.previous_queries[query_id] = timestamp_list
if len(timestamp_list) < 100:
return 0
return round(min(timestamp_list) + sliding_window - current_time) + 6
waittime = graphql_query_waittime(query_id)
if waittime > 0:
self._log('\nToo many queries in the last time. Need to wait {} seconds.'.format(waittime), flush=True)
time.sleep(waittime)
self._sleep() self._sleep()
response = tmpsession.get('https://www.instagram.com/graphql/query', try:
params={'query_id': query_id, response = tmpsession.get('https://www.instagram.com/graphql/query',
'variables': json.dumps(variables, separators=(',', ':'))}) params={'query_id': query_id,
if response.status_code != 200: 'variables': json.dumps(variables, separators=(',', ':'))})
raise ConnectionException("GraphQL query returned HTTP error code {}.".format(response.status_code)) except requests.exceptions.RequestException as err:
return response.json() print(err, file=sys.stderr)
if tries <= 1:
raise ConnectionException('GraphQL query {} failed multiple times with following variables:\n{}'
.format(query_id, json.dumps(variables, indent=4)))
self._sleep()
return self.graphql_query(query_id, variables, referer, tries - 1)
else:
if response.status_code != 200:
if response.status_code == 429:
print('GraphQL query returned HTTP error code 429 because too many queries occured '
'in the last time.', file=sys.stderr)
waittime = round(min(self.previous_queries.get(query_id))
+ sliding_window - datetime.now().timestamp()) + 6 \
if self.previous_queries.get(query_id) else sliding_window
if waittime > 0:
sys.stderr.flush()
self._log('Please do not use Instagram in your browser or run multiple instances '
'of Instaloader in parallel while using options that require GraphQL '
'queries.\nNeed to wait {} seconds.'.format(waittime), flush=True)
time.sleep(waittime)
return self.graphql_query(query_id, variables, referer, tries)
else:
raise ConnectionException("GraphQL query returned HTTP error code {}.".format(response.status_code))
return response.json()
finally:
timestamp_list = self.previous_queries.get(query_id)
if timestamp_list:
self.previous_queries.get(query_id).append(datetime.now().timestamp())
else:
self.previous_queries[query_id] = [datetime.now().timestamp()]
def get_username_by_id(self, profile_id: int) -> str: def get_username_by_id(self, profile_id: int) -> str:
"""To get the current username of a profile, given its unique ID, this function can be used.""" """To get the current username of a profile, given its unique ID, this function can be used."""
@@ -322,14 +372,25 @@ class Instaloader:
break break
return followees return followees
def get_comments(self, shortcode: str) -> List[Dict[str, Any]]: def get_comments(self, shortcode: str, tries: int = 3) -> List[Dict[str, Any]]:
"""Retrieve comments of node with given shortcode.""" """Retrieve comments of node with given shortcode."""
data = self.get_node_metadata(shortcode)
if data['edge_media_to_comment']['count'] == len(data['edge_media_to_comment']['edges']):
return [comment['node'] for comment in data['edge_media_to_comment']['edges']]
data = self.graphql_query(17852405266163336, {'shortcode': shortcode, data = self.graphql_query(17852405266163336, {'shortcode': shortcode,
'first': 500}, 'first': 500},
referer='https://www.instagram.com/p/' + shortcode + '/') referer='https://www.instagram.com/p/' + shortcode + '/')
comments = [] comments = []
while True: while True:
edge_media_to_comment = data['data']['shortcode_media']['edge_media_to_comment'] try:
edge_media_to_comment = data['data']['shortcode_media']['edge_media_to_comment']
except KeyError as err:
print('Missing key {} in response of GraphQL query for retrieving comments for node \'{}\'.'
.format(err, shortcode), file=sys.stderr)
if tries <= 1:
raise BadResponseException('GraphQL query returned strange JSON for shortcode {}:\n{}'.format(shortcode, data))
self._sleep()
return self.get_comments(shortcode, tries - 1)
comments.extend([comment['node'] for comment in edge_media_to_comment['edges']]) comments.extend([comment['node'] for comment in edge_media_to_comment['edges']])
page_info = edge_media_to_comment['page_info'] page_info = edge_media_to_comment['page_info']
if page_info['has_next_page']: if page_info['has_next_page']:
@@ -543,17 +604,22 @@ class Instaloader:
media = pic_json["entry_data"]["PostPage"][0]["graphql"]["shortcode_media"] \ media = pic_json["entry_data"]["PostPage"][0]["graphql"]["shortcode_media"] \
if "graphql" in pic_json["entry_data"]["PostPage"][0] \ if "graphql" in pic_json["entry_data"]["PostPage"][0] \
else pic_json["entry_data"]["PostPage"][0]["media"] else pic_json["entry_data"]["PostPage"][0]["media"]
except KeyError as err: except (KeyError, TypeError) as err:
print(err, file=sys.stderr) print(err, file=sys.stderr)
print(json.dumps(pic_json, indent=4), file=sys.stderr) print(json.dumps(pic_json, indent=4), file=sys.stderr)
if tries <= 1: if tries <= 1:
raise err raise NodeUnavailableException
self._sleep() self._sleep()
media = self.get_node_metadata(node_code, tries - 1) media = self.get_node_metadata(node_code, tries - 1)
return media return media
def get_location(self, node_code: str) -> Dict[str, str]: def get_location(self, node_code: str) -> Dict[str, str]:
media = self.get_node_metadata(node_code) try:
media = self.get_node_metadata(node_code)
except NodeUnavailableException:
print("Unable to lookup location for node \"https://www.instagram.com/p/{}/\".".format(node_code),
file=sys.stderr)
return dict()
if media["location"] is not None: if media["location"] is not None:
location_json = self.get_json("explore/locations/" + location_json = self.get_json("explore/locations/" +
media["location"]["id"]) media["location"]["id"])
@@ -580,8 +646,13 @@ class Instaloader:
if already_has_profilename: if already_has_profilename:
profilename = profile if profile is not None else node['owner']['username'] profilename = profile if profile is not None else node['owner']['username']
else: else:
metadata = self.get_node_metadata(shortcode) try:
profilename = metadata['owner']['username'] metadata = self.get_node_metadata(shortcode)
profilename = metadata['owner']['username']
except NodeUnavailableException:
print("Unable to gather profilename for node "
"\"https://www.instagram.com/p/{}/\".".format(shortcode), file=sys.stderr)
profilename = 'UNKNOWN'
else: else:
profilename = None profilename = None
profilename = profilename.lower() if profilename else None profilename = profilename.lower() if profilename else None
@@ -700,9 +771,9 @@ class Instaloader:
shortcode = item["code"] if "code" in item else "no_code" shortcode = item["code"] if "code" in item else "no_code"
date_float = item["device_timestamp"] if "device_timestamp" in item else item["taken_at"] date_float = item["device_timestamp"] if "device_timestamp" in item else item["taken_at"]
try: if date_float < 10000000000:
date = datetime.fromtimestamp(date_float) date = datetime.fromtimestamp(date_float)
except ValueError: else:
# device_timestamp seems to sometime be in milliseconds # device_timestamp seems to sometime be in milliseconds
date_float /= 1000 date_float /= 1000
date = datetime.fromtimestamp(date_float) date = datetime.fromtimestamp(date_float)
@@ -712,27 +783,32 @@ class Instaloader:
date=date, date=date,
shortcode=shortcode) shortcode=shortcode)
os.makedirs(os.path.dirname(filename), exist_ok=True) os.makedirs(os.path.dirname(filename), exist_ok=True)
if "image_versions2" in item: try:
url = item["image_versions2"]["candidates"][0]["url"] if "image_versions2" in item:
downloaded = self.download_pic(filename=filename, url = item["image_versions2"]["candidates"][0]["url"]
url=url, downloaded = self.download_pic(filename=filename,
mtime=date) url=url,
else: mtime=date)
self._log("Warning: Unable to find story image.") else:
downloaded = False self._log("Warning: Unable to find story image.")
if "caption" in item and item["caption"] is not None: downloaded = False
caption = item["caption"] if "caption" in item and item["caption"] is not None:
if isinstance(caption, dict) and "text" in caption: caption = item["caption"]
caption = caption["text"] if isinstance(caption, dict) and "text" in caption:
self.save_caption(filename, date, caption) caption = caption["text"]
else: self.save_caption(filename, date, caption)
self._log("<no caption>", end=' ', flush=True) else:
if "video_versions" in item and download_videos: self._log("<no caption>", end=' ', flush=True)
downloaded = self.download_pic(filename=filename, if "video_versions" in item and download_videos:
url=item["video_versions"][0]["url"], downloaded = self.download_pic(filename=filename,
mtime=date) url=item["video_versions"][0]["url"],
if "video_duration" in item and self.sleep and downloaded: mtime=date)
time.sleep(item["video_duration"]) if "video_duration" in item and self.sleep and downloaded:
time.sleep(item["video_duration"])
except NodeUnavailableException:
print("Unable to download node \"https://www.instagram.com/p/{}/\" of user {} from stories."
.format(shortcode, name), file=sys.stderr)
continue
if item["story_locations"]: if item["story_locations"]:
location = item["story_locations"][0]["location"] location = item["story_locations"][0]["location"]
if location: if location:
@@ -786,9 +862,14 @@ class Instaloader:
continue continue
self._log("[%3i] %s " % (count, name), end="", flush=True) self._log("[%3i] %s " % (count, name), end="", flush=True)
count += 1 count += 1
downloaded = self.download_node(node, profile=name, target=':feed', try:
download_videos=download_videos, geotags=geotags, downloaded = self.download_node(node, profile=name, target=':feed',
download_comments=download_comments) download_videos=download_videos, geotags=geotags,
download_comments=download_comments)
except NodeUnavailableException:
print("Unable to download node \"https://www.instagram.com/p/{}/\" of user {} from feed."
.format(node['shortcode'] if 'shortcode' in node else node['code'], name), file=sys.stderr)
continue
if fast_update and not downloaded: if fast_update and not downloaded:
return return
if not feed["page_info"]["has_next_page"]: if not feed["page_info"]["has_next_page"]:
@@ -830,9 +911,15 @@ class Instaloader:
self._log('<skipped>') self._log('<skipped>')
continue continue
count += 1 count += 1
downloaded = self.download_node(node=node, profile=None, target='#'+hashtag, try:
download_videos=download_videos, geotags=geotags, downloaded = self.download_node(node=node, profile=None, target='#'+hashtag,
download_comments=download_comments) download_videos=download_videos, geotags=geotags,
download_comments=download_comments)
except NodeUnavailableException:
print("Unable to download node \"https://www.instagram.com/p/{}/\" "
"while downloading hashtag \"{}\"."
.format(node['shortcode'] if 'shortcode' in node else node['code'], hashtag), file=sys.stderr)
continue
if fast_update and not downloaded: if fast_update and not downloaded:
return return
if data['entry_data']['TagPage'][0]['tag']['media']['page_info']['has_next_page']: if data['entry_data']['TagPage'][0]['tag']['media']['page_info']['has_next_page']:
@@ -904,7 +991,10 @@ class Instaloader:
name = name_updated name = name_updated
data = self.get_json(name) data = self.get_json(name)
# Download profile picture # Download profile picture
self.download_profilepic(name, data["entry_data"]["ProfilePage"][0]["user"]["profile_pic_url"]) try:
self.download_profilepic(name, data["entry_data"]["ProfilePage"][0]["user"]["profile_pic_url"])
except NodeUnavailableException:
print("Unable to download profilepic of user {}.".format(name), file=sys.stderr)
if profile_pic_only: if profile_pic_only:
return return
# Catch some errors # Catch some errors
@@ -939,9 +1029,14 @@ class Instaloader:
for node in data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"]: for node in data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"]:
self._log("[%3i/%3i] " % (count, totalcount), end="", flush=True) self._log("[%3i/%3i] " % (count, totalcount), end="", flush=True)
count += 1 count += 1
downloaded = self.download_node(node=node, profile=name, target=name, try:
download_videos=download_videos, geotags=geotags, downloaded = self.download_node(node=node, profile=name, target=name,
download_comments=download_comments) download_videos=download_videos, geotags=geotags,
download_comments=download_comments)
except NodeUnavailableException:
print("Unable to download node \"https://www.instagram.com/p/{}/\" of user {}."
.format(node['shortcode'] if 'shortcode' in node else node['code'], name), file=sys.stderr)
continue
if fast_update and not downloaded: if fast_update and not downloaded:
return return
data = self.get_json(name, max_id=get_last_id(data)) data = self.get_json(name, max_id=get_last_id(data))
+1 -1
View File
@@ -8,7 +8,7 @@ if sys.version_info < (3, 5):
setup( setup(
name='instaloader', name='instaloader',
version='2.2.1', version='2.2.3',
py_modules=['instaloader'], py_modules=['instaloader'],
url='https://github.com/Thammus/instaloader', url='https://github.com/Thammus/instaloader',
license='MIT', license='MIT',