Compare commits

...
15 Commits
Author SHA1 Message Date
Alexander Graf ece0d11915 Release of version 4.1.1 2018-10-07 15:17:51 +02:00
Alexander Graf 41e734e1b8 Fix fast_update in case of no download
If Instaloader was called with --fast-update and encountered a post where
nothing has been downloaded because of flags that disable download of certain
types of media, the download was aborted. This behavior is now fixed.

Closes #183.
2018-10-07 14:59:16 +02:00
Alexander Graf b7ea0d5953 download_profiles: Check errors only if required
Fixes #178, where with `--profile-pic-only` an error message is printed
(without any further implications) when downloading profile pictures of private
profiles without being logged in.
2018-09-23 10:09:08 +02:00
Alexander Graf 325e89c4b2 Print how many requests were made on 429 2018-09-06 09:17:38 +02:00
Alexander Graf e248020927 Release of version 4.1 2018-09-02 21:57:20 +02:00
Alexander Graf 5a7b76db31 Merge branch 'v4.1-dev' 2018-09-02 21:56:16 +02:00
Alexander Graf 1a239e17cb First Release Candidate for Version 4.1 2018-08-31 17:42:59 +02:00
Alexander Graf bb8749b753 Adjust rate control to current rate limits 2018-08-31 17:17:23 +02:00
Alexander Graf 1f311d3e1e First Beta Release for Version 4.1 2018-08-31 12:11:43 +02:00
Alexander Graf 862c51fa81 Profile.from_id graphql query is not rate limited 2018-08-30 13:59:46 +02:00
Alexander Graf cc15cb5857 Cache and reuse profiles for Profile.from_id() 2018-08-30 13:52:16 +02:00
Alexander Graf 18707ee232 docs: Better explanation for is_video
Closes #173.
2018-08-30 10:16:56 +02:00
Alexander Graf 0e534ba519 Use username/:tagged as target for tagged posts
Subdirs in profile folders were introduced with --highlights, and moving
:tagged posts there allows to further call instaloader as

instaloader [flags] */

(#154)
2018-08-30 09:57:42 +02:00
Alexander Graf 084424e856 docs/codesnippets: Link to source tree on github 2018-08-30 09:18:58 +02:00
Alexander Graf 567a04a661 format_filename method to apply filename pattern 2018-08-30 09:13:49 +02:00
8 changed files with 48 additions and 17 deletions
+9 -5
View File
@@ -192,11 +192,6 @@ where the attributes from :class:`Post` or
Id est, the following attributes can be used with both Id est, the following attributes can be used with both
:option:`--post-filter` and :option:`--storyitem-filter`: :option:`--post-filter` and :option:`--storyitem-filter`:
- :attr:`~Post.is_video` (bool)
Post/StoryItem is a video. For example, you may skip videos::
instaloader --post-filter="not is_video" target
- :attr:`~Post.owner_username` (str), :attr:`~Post.owner_id` (int) - :attr:`~Post.owner_username` (str), :attr:`~Post.owner_id` (int)
Owner profile username / userid. Owner profile username / userid.
@@ -206,6 +201,15 @@ Id est, the following attributes can be used with both
instaloader --post-filter="date_utc <= datetime(2018, 5, 31)" target instaloader --post-filter="date_utc <= datetime(2018, 5, 31)" target
- :attr:`~Post.is_video` (bool)
Post/StoryItem is a video. For example, you may skip videos::
instaloader --post-filter="not is_video" target
This is not the same as :option:`--no-videos` and
:option:`--no-video-thumbnails`, since sidecar posts (posts that contain
multiple pictures/videos in one post) have this attribute set to False.
As :option:`--post-filter`, the following attributes can be used additionally: As :option:`--post-filter`, the following attributes can be used additionally:
- :attr:`~Post.viewer_has_liked` (bool) - :attr:`~Post.viewer_has_liked` (bool)
+3
View File
@@ -14,6 +14,9 @@ Here we present code examples that use the :ref:`python-module-instaloader` for
more advanced Instagram downloading or metadata mining than what is possible more advanced Instagram downloading or metadata mining than what is possible
with the Instaloader command line interface. with the Instaloader command line interface.
The scripts presented here can be downloaded from our source tree:
`instaloader/docs/codesnippets/ <https://github.com/instaloader/instaloader/tree/master/docs/codesnippets>`__
.. For each code snippet: .. For each code snippet:
- title - title
- brief description of what it does / motivation / how it works - brief description of what it does / motivation / how it works
+4
View File
@@ -0,0 +1,4 @@
# Advanced Instaloader Examples
This directory contains the scripts that are presented at:
https://instaloader.github.io/codesnippets.html
+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.1a1' __version__ = '4.1.1'
try: try:
+14 -8
View File
@@ -338,6 +338,12 @@ class Instaloader:
:raises ConnectionException: If connection to Instagram failed.""" :raises ConnectionException: If connection to Instagram failed."""
self.context.login(user, passwd) self.context.login(user, passwd)
def format_filename(self, item: Union[Post, StoryItem], target: Optional[str] = None):
"""Format filename of a :class:`Post` or :class:`StoryItem` according to ``filename-pattern`` parameter.
.. versionadded:: 4.1"""
return _PostPathFormatter(item).format(self.filename_pattern, target=target)
def download_post(self, post: Post, target: str) -> bool: def download_post(self, post: Post, target: str) -> bool:
""" """
Download everything associated with one instagram post node, i.e. picture, caption and video. Download everything associated with one instagram post node, i.e. picture, caption and video.
@@ -348,22 +354,22 @@ class Instaloader:
""" """
dirname = _PostPathFormatter(post).format(self.dirname_pattern, target=target) dirname = _PostPathFormatter(post).format(self.dirname_pattern, target=target)
filename = dirname + '/' + _PostPathFormatter(post).format(self.filename_pattern, target=target) filename = dirname + '/' + self.format_filename(post, target=target)
os.makedirs(os.path.dirname(filename), exist_ok=True) os.makedirs(os.path.dirname(filename), exist_ok=True)
# Download the image(s) / video thumbnail and videos within sidecars if desired # Download the image(s) / video thumbnail and videos within sidecars if desired
downloaded = False downloaded = True
if self.download_pictures: if self.download_pictures:
if post.typename == 'GraphSidecar': if post.typename == 'GraphSidecar':
edge_number = 1 edge_number = 1
for sidecar_node in post.get_sidecar_nodes(): for sidecar_node in post.get_sidecar_nodes():
# Download picture or video thumbnail # Download picture or video thumbnail
if not sidecar_node.is_video or self.download_video_thumbnails is True: if not sidecar_node.is_video or self.download_video_thumbnails is True:
downloaded |= self.download_pic(filename=filename, url=sidecar_node.display_url, downloaded &= self.download_pic(filename=filename, url=sidecar_node.display_url,
mtime=post.date_local, filename_suffix=str(edge_number)) mtime=post.date_local, filename_suffix=str(edge_number))
# Additionally download video if available and desired # Additionally download video if available and desired
if sidecar_node.is_video and self.download_videos is True: if sidecar_node.is_video and self.download_videos is True:
downloaded |= self.download_pic(filename=filename, url=sidecar_node.video_url, downloaded &= self.download_pic(filename=filename, url=sidecar_node.video_url,
mtime=post.date_local, filename_suffix=str(edge_number)) mtime=post.date_local, filename_suffix=str(edge_number))
edge_number += 1 edge_number += 1
elif post.typename == 'GraphImage': elif post.typename == 'GraphImage':
@@ -381,7 +387,7 @@ class Instaloader:
# Download video if desired # Download video if desired
if post.is_video and self.download_videos is True: if post.is_video and self.download_videos is True:
downloaded |= self.download_pic(filename=filename, url=post.video_url, mtime=post.date_local) downloaded &= self.download_pic(filename=filename, url=post.video_url, mtime=post.date_local)
# Download geotags if desired # Download geotags if desired
if self.download_geotags and post.location: if self.download_geotags and post.location:
@@ -473,7 +479,7 @@ class Instaloader:
date_local = item.date_local date_local = item.date_local
dirname = _PostPathFormatter(item).format(self.dirname_pattern, target=target) dirname = _PostPathFormatter(item).format(self.dirname_pattern, target=target)
filename = dirname + '/' + _PostPathFormatter(item).format(self.filename_pattern, target=target) filename = dirname + '/' + self.format_filename(item, target=target)
os.makedirs(os.path.dirname(filename), exist_ok=True) os.makedirs(os.path.dirname(filename), exist_ok=True)
downloaded = False downloaded = False
if not item.is_video or self.download_video_thumbnails is True: if not item.is_video or self.download_video_thumbnails is True:
@@ -693,7 +699,7 @@ class Instaloader:
.. versionadded:: 4.1""" .. versionadded:: 4.1"""
if target is None: if target is None:
target = profile.username + ':tagged' target = profile.username + '/:tagged'
self.context.log("Retrieving tagged posts for profile {}.".format(profile.username)) self.context.log("Retrieving tagged posts for profile {}.".format(profile.username))
count = 1 count = 1
for post in profile.get_tagged_posts(): for post in profile.get_tagged_posts():
@@ -817,7 +823,7 @@ class Instaloader:
self.download_profilepic(profile) self.download_profilepic(profile)
# Catch some errors # Catch some errors
if profile.is_private: if profile.is_private and (tagged or highlights or posts):
if not self.context.is_logged_in: if not self.context.is_logged_in:
raise LoginRequiredException("--login=USERNAME required.") raise LoginRequiredException("--login=USERNAME required.")
if not profile.followed_by_viewer and self.context.username != profile.username: if not profile.followed_by_viewer and self.context.username != profile.username:
+6 -2
View File
@@ -57,7 +57,7 @@ class InstaloaderContext:
self.quiet = quiet self.quiet = quiet
self.max_connection_attempts = max_connection_attempts self.max_connection_attempts = max_connection_attempts
self._graphql_page_length = 50 self._graphql_page_length = 50
self.graphql_count_per_slidingwindow = graphql_count_per_slidingwindow or 20 self.graphql_count_per_slidingwindow = graphql_count_per_slidingwindow or 200
self._root_rhx_gis = None self._root_rhx_gis = None
# error log, filled with error() and printed at the end of Instaloader.main() # error log, filled with error() and printed at the end of Instaloader.main()
@@ -69,6 +69,9 @@ class InstaloaderContext:
# Can be set to True for testing, disables supression of InstaloaderContext._error_catcher # Can be set to True for testing, disables supression of InstaloaderContext._error_catcher
self.raise_all_errors = False self.raise_all_errors = False
# Cache profile from id (mapping from id to Profile)
self.profile_id_cache = dict()
@contextmanager @contextmanager
def anonymous_copy(self): def anonymous_copy(self):
session = self._session session = self._session
@@ -218,7 +221,7 @@ class InstaloaderContext:
def _sleep(self): def _sleep(self):
"""Sleep a short time if self.sleep is set. Called before each request to instagram.com.""" """Sleep a short time if self.sleep is set. Called before each request to instagram.com."""
if self.sleep: if self.sleep:
time.sleep(min(random.expovariate(0.6), 5.0)) time.sleep(min(random.expovariate(0.7), 5.0))
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) -> Dict[str, Any]: session: Optional[requests.Session] = None, _attempt=1) -> Dict[str, Any]:
@@ -303,6 +306,7 @@ class InstaloaderContext:
if isinstance(err, TooManyRequestsException): if isinstance(err, TooManyRequestsException):
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:
print("Made {} GraphQL requests.".format(len(self.query_timestamps)), file=sys.stderr)
waittime = graphql_query_waittime(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, at {:%H:%M}.' self.log('The request will be retried in {} seconds, at {:%H:%M}.'
+5 -1
View File
@@ -402,6 +402,8 @@ class Profile:
:param profile_id: userid :param profile_id: userid
:raises: :class:`ProfileNotExistsException`, :class:`ProfileHasNoPicsException` :raises: :class:`ProfileNotExistsException`, :class:`ProfileHasNoPicsException`
""" """
if profile_id in context.profile_id_cache:
return context.profile_id_cache[profile_id]
data = context.graphql_query("472f257a40c653c64c666ce877d59d2b", data = context.graphql_query("472f257a40c653c64c666ce877d59d2b",
{'id': str(profile_id), 'first': 1}, {'id': str(profile_id), 'first': 1},
rhx_gis=context.root_rhx_gis)['data']['user'] rhx_gis=context.root_rhx_gis)['data']['user']
@@ -415,7 +417,9 @@ class Profile:
raise ProfileHasNoPicsException("Profile with ID {0}: no pics found.".format(str(profile_id))) raise ProfileHasNoPicsException("Profile with ID {0}: no pics found.".format(str(profile_id)))
else: else:
raise LoginRequiredException("Login required to determine username (ID: " + str(profile_id) + ").") raise LoginRequiredException("Login required to determine username (ID: " + str(profile_id) + ").")
return Post(context, data['edges'][0]['node']).owner_profile profile = Post(context, data['edges'][0]['node']).owner_profile
context.profile_id_cache[profile_id] = profile
return profile
def _asdict(self): def _asdict(self):
json_node = self._node.copy() json_node = self._node.copy()
+6
View File
@@ -63,6 +63,12 @@ class TestInstaloaderAnonymously(unittest.TestCase):
def test_hashtag_download(self): def test_hashtag_download(self):
self.L.download_hashtag(HASHTAG, NORMAL_MAX_COUNT) self.L.download_hashtag(HASHTAG, NORMAL_MAX_COUNT)
def test_hashtag_paging(self):
for count, post in enumerate(self.L.get_hashtag_posts(HASHTAG)):
print(post)
if count == PAGING_MAX_COUNT:
break
def test_get_id_by_username(self): def test_get_id_by_username(self):
self.assertEqual(PUBLIC_PROFILE_ID, self.assertEqual(PUBLIC_PROFILE_ID,
instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).userid) instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).userid)