mirror of
https://github.com/instaloader/instaloader.git
synced 2026-10-23 17:09:37 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
438669b954 | ||
|
|
a7a05c200c | ||
|
|
f53bdf9ef8 | ||
|
|
13612c606e |
@@ -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.5.1'
|
__version__ = '4.5.2'
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
+11
-10
@@ -514,15 +514,16 @@ class Instaloader:
|
|||||||
# 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 = True
|
downloaded = True
|
||||||
if post.typename == 'GraphSidecar':
|
if post.typename == 'GraphSidecar':
|
||||||
for edge_number, sidecar_node in enumerate(post.get_sidecar_nodes(), start=1):
|
if self.download_pictures or self.download_videos:
|
||||||
if self.download_pictures and (not sidecar_node.is_video or self.download_video_thumbnails):
|
for edge_number, sidecar_node in enumerate(post.get_sidecar_nodes(), start=1):
|
||||||
# Download sidecar picture or video thumbnail (--no-pictures implies --no-video-thumbnails)
|
if self.download_pictures and (not sidecar_node.is_video or self.download_video_thumbnails):
|
||||||
downloaded &= self.download_pic(filename=filename, url=sidecar_node.display_url,
|
# Download sidecar picture or video thumbnail (--no-pictures implies --no-video-thumbnails)
|
||||||
mtime=post.date_local, filename_suffix=str(edge_number))
|
downloaded &= self.download_pic(filename=filename, url=sidecar_node.display_url,
|
||||||
if sidecar_node.is_video and self.download_videos:
|
mtime=post.date_local, filename_suffix=str(edge_number))
|
||||||
# Download sidecar video if desired
|
if sidecar_node.is_video and self.download_videos:
|
||||||
downloaded &= self.download_pic(filename=filename, url=sidecar_node.video_url,
|
# Download sidecar video if desired
|
||||||
mtime=post.date_local, filename_suffix=str(edge_number))
|
downloaded &= self.download_pic(filename=filename, url=sidecar_node.video_url,
|
||||||
|
mtime=post.date_local, filename_suffix=str(edge_number))
|
||||||
elif post.typename == 'GraphImage':
|
elif post.typename == 'GraphImage':
|
||||||
# Download picture
|
# Download picture
|
||||||
if self.download_pictures:
|
if self.download_pictures:
|
||||||
@@ -856,7 +857,7 @@ class Instaloader:
|
|||||||
"""
|
"""
|
||||||
self.context.log("Retrieving saved posts...")
|
self.context.log("Retrieving saved posts...")
|
||||||
assert self.context.username is not None # safe due to @_requires_login; required by typechecker
|
assert self.context.username is not None # safe due to @_requires_login; required by typechecker
|
||||||
node_iterator = Profile.from_username(self.context, self.context.username).get_saved_posts()
|
node_iterator = Profile.own_profile(self.context).get_saved_posts()
|
||||||
self.posts_download_loop(node_iterator, ":saved",
|
self.posts_download_loop(node_iterator, ":saved",
|
||||||
fast_update, post_filter,
|
fast_update, post_filter,
|
||||||
max_count=max_count, total_count=node_iterator.count)
|
max_count=max_count, total_count=node_iterator.count)
|
||||||
|
|||||||
@@ -549,8 +549,8 @@ class RateController:
|
|||||||
|
|
||||||
def __init__(self, context: InstaloaderContext):
|
def __init__(self, context: InstaloaderContext):
|
||||||
self._context = context
|
self._context = context
|
||||||
self._graphql_query_timestamps = dict() # type: Dict[str, List[float]]
|
self._query_timestamps = dict() # type: Dict[str, List[float]]
|
||||||
self._graphql_earliest_next_request_time = 0.0
|
self._earliest_next_request_time = 0.0
|
||||||
|
|
||||||
def sleep(self, secs: float):
|
def sleep(self, secs: float):
|
||||||
"""Wait given number of seconds."""
|
"""Wait given number of seconds."""
|
||||||
@@ -560,11 +560,11 @@ class RateController:
|
|||||||
time.sleep(secs)
|
time.sleep(secs)
|
||||||
|
|
||||||
def _dump_query_timestamps(self, current_time: float, failed_query_type: str):
|
def _dump_query_timestamps(self, current_time: float, failed_query_type: str):
|
||||||
windows = [10, 11, 15, 20, 30, 60]
|
windows = [10, 11, 20, 22, 30, 60]
|
||||||
self._context.error("Requests within last {} minutes grouped by type:"
|
self._context.error("Requests within last {} minutes grouped by type:"
|
||||||
.format('/'.join(str(w) for w in windows)),
|
.format('/'.join(str(w) for w in windows)),
|
||||||
repeat_at_end=False)
|
repeat_at_end=False)
|
||||||
for query_type, times in self._graphql_query_timestamps.items():
|
for query_type, times in self._query_timestamps.items():
|
||||||
reqs_in_sliding_window = [sum(t > current_time - w * 60 for t in times) for w in windows]
|
reqs_in_sliding_window = [sum(t > current_time - w * 60 for t in times) for w in windows]
|
||||||
self._context.error(" {} {:>32}: {}".format(
|
self._context.error(" {} {:>32}: {}".format(
|
||||||
"*" if query_type == failed_query_type else " ",
|
"*" if query_type == failed_query_type else " ",
|
||||||
@@ -573,28 +573,61 @@ class RateController:
|
|||||||
), repeat_at_end=False)
|
), repeat_at_end=False)
|
||||||
|
|
||||||
def count_per_sliding_window(self, query_type: str) -> int:
|
def count_per_sliding_window(self, query_type: str) -> int:
|
||||||
"""Return how many GraphQL requests can be done within the sliding window."""
|
"""Return how many requests can be done within the sliding window."""
|
||||||
# Not static, to allow for the count_per_sliding_window to depend on context-inherent properties, such as
|
# Not static, to allow for the count_per_sliding_window to depend on context-inherent properties, such as
|
||||||
# whether we are logged in.
|
# whether we are logged in.
|
||||||
# pylint:disable=no-self-use,unused-argument
|
# pylint:disable=no-self-use
|
||||||
return 200
|
return 100 if query_type in ['iphone', 'other'] else 200
|
||||||
|
|
||||||
|
def _reqs_in_sliding_window(self, query_type: Optional[str], current_time: float, window: float) -> List[float]:
|
||||||
|
if query_type is not None:
|
||||||
|
# timestamps of type query_type
|
||||||
|
relevant_timestamps = self._query_timestamps[query_type]
|
||||||
|
else:
|
||||||
|
# all GraphQL queries, i.e. not 'iphone' or 'other'
|
||||||
|
graphql_query_timestamps = filter(lambda tp: tp[0] not in ['iphone', 'other'],
|
||||||
|
self._query_timestamps.items())
|
||||||
|
relevant_timestamps = [t for times in (tp[1] for tp in graphql_query_timestamps) for t in times]
|
||||||
|
return list(filter(lambda t: t > current_time - window, relevant_timestamps))
|
||||||
|
|
||||||
def query_waittime(self, query_type: str, current_time: float, untracked_queries: bool = False) -> float:
|
def query_waittime(self, query_type: str, current_time: float, untracked_queries: bool = False) -> float:
|
||||||
"""Calculate time needed to wait before GraphQL query can be executed."""
|
"""Calculate time needed to wait before query can be executed."""
|
||||||
sliding_window = 660
|
per_type_sliding_window = 660
|
||||||
if query_type not in self._graphql_query_timestamps:
|
if query_type not in self._query_timestamps:
|
||||||
self._graphql_query_timestamps[query_type] = []
|
self._query_timestamps[query_type] = []
|
||||||
self._graphql_query_timestamps[query_type] = list(filter(lambda t: t > current_time - 60 * 60,
|
self._query_timestamps[query_type] = list(filter(lambda t: t > current_time - 60 * 60,
|
||||||
self._graphql_query_timestamps[query_type]))
|
self._query_timestamps[query_type]))
|
||||||
reqs_in_sliding_window = list(filter(lambda t: t > current_time - sliding_window,
|
|
||||||
self._graphql_query_timestamps[query_type]))
|
def per_type_next_request_time():
|
||||||
count_per_sliding_window = self.count_per_sliding_window(query_type)
|
reqs_in_sliding_window = self._reqs_in_sliding_window(query_type, current_time, per_type_sliding_window)
|
||||||
if len(reqs_in_sliding_window) < count_per_sliding_window and not untracked_queries:
|
if len(reqs_in_sliding_window) < self.count_per_sliding_window(query_type):
|
||||||
return max(0.0, self._graphql_earliest_next_request_time - current_time)
|
return 0.0
|
||||||
next_request_time = min(reqs_in_sliding_window) + sliding_window + 6
|
else:
|
||||||
if untracked_queries:
|
return min(reqs_in_sliding_window) + per_type_sliding_window + 6
|
||||||
self._graphql_earliest_next_request_time = next_request_time
|
|
||||||
return max(next_request_time, self._graphql_earliest_next_request_time) - current_time
|
def gql_accumulated_next_request_time():
|
||||||
|
if query_type in ['iphone', 'other']:
|
||||||
|
return 0.0
|
||||||
|
gql_accumulated_sliding_window = 600
|
||||||
|
gql_accumulated_max_count = 275
|
||||||
|
reqs_in_sliding_window = self._reqs_in_sliding_window(None, current_time, gql_accumulated_sliding_window)
|
||||||
|
if len(reqs_in_sliding_window) < gql_accumulated_max_count:
|
||||||
|
return 0.0
|
||||||
|
else:
|
||||||
|
return min(reqs_in_sliding_window) + gql_accumulated_sliding_window
|
||||||
|
|
||||||
|
def untracked_next_request_time():
|
||||||
|
if untracked_queries:
|
||||||
|
reqs_in_sliding_window = self._reqs_in_sliding_window(query_type, current_time, per_type_sliding_window)
|
||||||
|
self._earliest_next_request_time = min(reqs_in_sliding_window) + per_type_sliding_window + 6
|
||||||
|
return self._earliest_next_request_time
|
||||||
|
|
||||||
|
return max(0.0,
|
||||||
|
max(
|
||||||
|
per_type_next_request_time(),
|
||||||
|
gql_accumulated_next_request_time(),
|
||||||
|
untracked_next_request_time(),
|
||||||
|
) - current_time)
|
||||||
|
|
||||||
def wait_before_query(self, query_type: str) -> None:
|
def wait_before_query(self, query_type: str) -> None:
|
||||||
"""This method is called before a query to Instagram. It calls :meth:`RateController.sleep` to wait
|
"""This method is called before a query to Instagram. It calls :meth:`RateController.sleep` to wait
|
||||||
@@ -606,10 +639,10 @@ class RateController:
|
|||||||
.format(round(waittime), datetime.now() + timedelta(seconds=waittime)))
|
.format(round(waittime), datetime.now() + timedelta(seconds=waittime)))
|
||||||
if waittime > 0:
|
if waittime > 0:
|
||||||
self.sleep(waittime)
|
self.sleep(waittime)
|
||||||
if query_type not in self._graphql_query_timestamps:
|
if query_type not in self._query_timestamps:
|
||||||
self._graphql_query_timestamps[query_type] = [time.monotonic()]
|
self._query_timestamps[query_type] = [time.monotonic()]
|
||||||
else:
|
else:
|
||||||
self._graphql_query_timestamps[query_type].append(time.monotonic())
|
self._query_timestamps[query_type].append(time.monotonic())
|
||||||
|
|
||||||
def handle_429(self, query_type: str) -> None:
|
def handle_429(self, query_type: str) -> None:
|
||||||
"""This method is called to handle a 429 Too Many Requests response. It calls :meth:`RateController.sleep` to
|
"""This method is called to handle a 429 Too Many Requests response. It calls :meth:`RateController.sleep` to
|
||||||
|
|||||||
@@ -81,11 +81,13 @@ class NodeIterator(Iterator[T]):
|
|||||||
self._node_wrapper = node_wrapper
|
self._node_wrapper = node_wrapper
|
||||||
self._query_variables = query_variables if query_variables is not None else {}
|
self._query_variables = query_variables if query_variables is not None else {}
|
||||||
self._query_referer = query_referer
|
self._query_referer = query_referer
|
||||||
self._data = first_data
|
|
||||||
self._page_index = 0
|
self._page_index = 0
|
||||||
self._total_index = 0
|
self._total_index = 0
|
||||||
self._best_before = (None if first_data is None else
|
if first_data is not None:
|
||||||
datetime.now() + NodeIterator._shelf_life)
|
self._data = first_data
|
||||||
|
self._best_before = datetime.now() + NodeIterator._shelf_life
|
||||||
|
else:
|
||||||
|
self._data = self._query()
|
||||||
|
|
||||||
def _query(self, after: Optional[str] = None) -> Dict:
|
def _query(self, after: Optional[str] = None) -> Dict:
|
||||||
pagination_variables = {'first': NodeIterator._graphql_page_length} # type: Dict[str, Any]
|
pagination_variables = {'first': NodeIterator._graphql_page_length} # type: Dict[str, Any]
|
||||||
@@ -113,8 +115,6 @@ class NodeIterator(Iterator[T]):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
def __next__(self) -> T:
|
def __next__(self) -> T:
|
||||||
if self._data is None:
|
|
||||||
self._data = self._query()
|
|
||||||
if self._page_index < len(self._data['edges']):
|
if self._page_index < len(self._data['edges']):
|
||||||
node = self._data['edges'][self._page_index]['node']
|
node = self._data['edges'][self._page_index]['node']
|
||||||
page_index, total_index = self._page_index, self._total_index
|
page_index, total_index = self._page_index, self._total_index
|
||||||
@@ -193,8 +193,12 @@ class NodeIterator(Iterator[T]):
|
|||||||
self._query_referer != frozen.query_referer or
|
self._query_referer != frozen.query_referer or
|
||||||
self._context.username != frozen.context_username):
|
self._context.username != frozen.context_username):
|
||||||
raise InvalidArgumentException("Mismatching resume information.")
|
raise InvalidArgumentException("Mismatching resume information.")
|
||||||
|
if not frozen.best_before:
|
||||||
|
raise InvalidArgumentException("\"best before\" date missing.")
|
||||||
|
if frozen.remaining_data is None:
|
||||||
|
raise InvalidArgumentException("\"remaining_data\" missing.")
|
||||||
self._total_index = frozen.total_index
|
self._total_index = frozen.total_index
|
||||||
self._best_before = datetime.fromtimestamp(frozen.best_before) if frozen.best_before else None
|
self._best_before = datetime.fromtimestamp(frozen.best_before)
|
||||||
self._data = frozen.remaining_data
|
self._data = frozen.remaining_data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -579,6 +579,17 @@ class Profile:
|
|||||||
context.profile_id_cache[profile_id] = profile
|
context.profile_id_cache[profile_id] = profile
|
||||||
return profile
|
return profile
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def own_profile(cls, context: InstaloaderContext):
|
||||||
|
"""Return own profile if logged-in.
|
||||||
|
|
||||||
|
:param context: :attr:`Instaloader.context`
|
||||||
|
|
||||||
|
.. versionadded:: 4.5.2"""
|
||||||
|
if not context.is_logged_in:
|
||||||
|
raise LoginRequiredException("--login required to access own profile.")
|
||||||
|
return cls(context, context.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {})["data"]["user"])
|
||||||
|
|
||||||
def _asdict(self):
|
def _asdict(self):
|
||||||
json_node = self._node.copy()
|
json_node = self._node.copy()
|
||||||
# remove posts to avoid "Circular reference detected" exception
|
# remove posts to avoid "Circular reference detected" exception
|
||||||
@@ -802,7 +813,6 @@ class Profile:
|
|||||||
if self.username != self._context.username:
|
if self.username != self._context.username:
|
||||||
raise LoginRequiredException("--login={} required to get that profile's saved posts.".format(self.username))
|
raise LoginRequiredException("--login={} required to get that profile's saved posts.".format(self.username))
|
||||||
|
|
||||||
self._obtain_metadata()
|
|
||||||
return NodeIterator(
|
return NodeIterator(
|
||||||
self._context,
|
self._context,
|
||||||
'f883d95537fbcd400f466f63d42bd8a1',
|
'f883d95537fbcd400f466f63d42bd8a1',
|
||||||
@@ -810,7 +820,6 @@ class Profile:
|
|||||||
lambda n: Post(self._context, n),
|
lambda n: Post(self._context, n),
|
||||||
{'id': self.userid},
|
{'id': self.userid},
|
||||||
'https://www.instagram.com/{0}/'.format(self.username),
|
'https://www.instagram.com/{0}/'.format(self.username),
|
||||||
self._metadata('edge_saved_media'),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_tagged_posts(self) -> NodeIterator[Post]:
|
def get_tagged_posts(self) -> NodeIterator[Post]:
|
||||||
|
|||||||
Reference in New Issue
Block a user