mirror of
https://github.com/instaloader/instaloader.git
synced 2026-10-23 17:09:37 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cc29a4ceb | ||
|
|
ffb0aea5eb | ||
|
|
84bb60c155 | ||
|
|
7857909886 | ||
|
|
2206a7e5d7 | ||
|
|
f1b5e9cdf1 | ||
|
|
8debcbb7ab | ||
|
|
e97fd8a27c | ||
|
|
7f9d1618ad | ||
|
|
6a42906697 | ||
|
|
4bb32e3c5a |
@@ -18,7 +18,7 @@ jobs:
|
|||||||
stale-issue-message: 'There has been no activity on this question for an extended period of time. This issue will be closed after further 14 days of inactivity.'
|
stale-issue-message: 'There has been no activity on this question for an extended period of time. This issue will be closed after further 14 days of inactivity.'
|
||||||
stale-issue-label: 'stale'
|
stale-issue-label: 'stale'
|
||||||
exempt-issue-labels: 'leave open'
|
exempt-issue-labels: 'leave open'
|
||||||
days-before-stale: 15
|
days-before-stale: 21
|
||||||
days-before-close: -1
|
days-before-close: -1
|
||||||
remove-stale-when-updated: false
|
remove-stale-when-updated: false
|
||||||
- uses: actions/stale@v1
|
- uses: actions/stale@v1
|
||||||
@@ -30,5 +30,5 @@ jobs:
|
|||||||
stale-pr-label: 'stale'
|
stale-pr-label: 'stale'
|
||||||
exempt-issue-label: 'leave open'
|
exempt-issue-label: 'leave open'
|
||||||
exempt-pr-label: 'leave open'
|
exempt-pr-label: 'leave open'
|
||||||
days-before-stale: 135
|
days-before-stale: 189
|
||||||
days-before-close: 14
|
days-before-close: 14
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ Supporters
|
|||||||
|
|
||||||
.. current-sponsors-start
|
.. current-sponsors-start
|
||||||
|
|
||||||
|
| Instaloader is proudly sponsored by
|
||||||
|
| `@socialmethod <https://github.com/socialmethod>`__
|
||||||
|
|
||||||
See `Alex' GitHub Sponsors <https://github.com/sponsors/aandergr>`__ page for
|
See `Alex' GitHub Sponsors <https://github.com/sponsors/aandergr>`__ page for
|
||||||
how you can sponsor the development of Instaloader!
|
how you can sponsor the development of Instaloader!
|
||||||
|
|
||||||
|
|||||||
@@ -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.9.2'
|
__version__ = '4.9.5'
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -478,9 +478,8 @@ class Instaloader:
|
|||||||
self.context.log(pcaption, end=' ', flush=True)
|
self.context.log(pcaption, end=' ', flush=True)
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
self.context.log('txt', end=' ', flush=True)
|
self.context.log('txt', end=' ', flush=True)
|
||||||
with open(filename, 'wb') as text_file:
|
with open(filename, 'w', encoding='UTF-8') as fio:
|
||||||
with BytesIO(bcaption) as bio:
|
fio.write(caption)
|
||||||
shutil.copyfileobj(cast(IO, bio), text_file)
|
|
||||||
os.utime(filename, (datetime.now().timestamp(), mtime.timestamp()))
|
os.utime(filename, (datetime.now().timestamp(), mtime.timestamp()))
|
||||||
|
|
||||||
def save_location(self, filename: str, location: PostLocation, mtime: datetime) -> None:
|
def save_location(self, filename: str, location: PostLocation, mtime: datetime) -> None:
|
||||||
@@ -563,7 +562,7 @@ class Instaloader:
|
|||||||
if latest_stamps is None:
|
if latest_stamps is None:
|
||||||
self.download_profilepic(profile)
|
self.download_profilepic(profile)
|
||||||
return
|
return
|
||||||
profile_pic_basename = profile.profile_pic_url.split('/')[-1].split('?')[0]
|
profile_pic_basename = profile.profile_pic_url_no_iphone.split('/')[-1].split('?')[0]
|
||||||
saved_basename = latest_stamps.get_profile_pic(profile.username)
|
saved_basename = latest_stamps.get_profile_pic(profile.username)
|
||||||
if saved_basename == profile_pic_basename:
|
if saved_basename == profile_pic_basename:
|
||||||
return
|
return
|
||||||
@@ -879,13 +878,18 @@ class Instaloader:
|
|||||||
filename_template = os.path.join(dirname, self.format_filename(item, target=target))
|
filename_template = os.path.join(dirname, self.format_filename(item, target=target))
|
||||||
filename = self.__prepare_filename(filename_template, lambda: item.url)
|
filename = self.__prepare_filename(filename_template, lambda: item.url)
|
||||||
downloaded = False
|
downloaded = False
|
||||||
if not item.is_video or self.download_video_thumbnails is True:
|
video_url_fetch_failed = False
|
||||||
|
if item.is_video and self.download_videos is True:
|
||||||
|
video_url = item.video_url
|
||||||
|
if video_url:
|
||||||
|
filename = self.__prepare_filename(filename_template, lambda: str(video_url))
|
||||||
|
downloaded |= (not _already_downloaded(filename + ".mp4") and
|
||||||
|
self.download_pic(filename=filename, url=video_url, mtime=date_local))
|
||||||
|
else:
|
||||||
|
video_url_fetch_failed = True
|
||||||
|
if video_url_fetch_failed or not item.is_video or self.download_video_thumbnails is True:
|
||||||
downloaded = (not _already_downloaded(filename + ".jpg") and
|
downloaded = (not _already_downloaded(filename + ".jpg") and
|
||||||
self.download_pic(filename=filename, url=item.url, mtime=date_local))
|
self.download_pic(filename=filename, url=item.url, mtime=date_local))
|
||||||
if item.is_video and self.download_videos is True:
|
|
||||||
filename = self.__prepare_filename(filename_template, lambda: str(item.video_url))
|
|
||||||
downloaded |= (not _already_downloaded(filename + ".mp4") and
|
|
||||||
self.download_pic(filename=filename, url=item.video_url, mtime=date_local))
|
|
||||||
# Save caption if desired
|
# Save caption if desired
|
||||||
metadata_string = _ArbitraryItemFormatter(item).format(self.storyitem_metadata_txt_pattern).strip()
|
metadata_string = _ArbitraryItemFormatter(item).format(self.storyitem_metadata_txt_pattern).strip()
|
||||||
if metadata_string:
|
if metadata_string:
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class NodeIterator(Iterator[T]):
|
|||||||
query_variables: Optional[Dict[str, Any]] = None,
|
query_variables: Optional[Dict[str, Any]] = None,
|
||||||
query_referer: Optional[str] = None,
|
query_referer: Optional[str] = None,
|
||||||
first_data: Optional[Dict[str, Any]] = None,
|
first_data: Optional[Dict[str, Any]] = None,
|
||||||
is_first: Optional[Callable[[T], bool]] = None):
|
is_first: Optional[Callable[[T, Optional[T]], bool]] = None):
|
||||||
self._context = context
|
self._context = context
|
||||||
self._query_hash = query_hash
|
self._query_hash = query_hash
|
||||||
self._edge_extractor = edge_extractor
|
self._edge_extractor = edge_extractor
|
||||||
@@ -131,7 +131,7 @@ class NodeIterator(Iterator[T]):
|
|||||||
raise
|
raise
|
||||||
item = self._node_wrapper(node)
|
item = self._node_wrapper(node)
|
||||||
if self._is_first is not None:
|
if self._is_first is not None:
|
||||||
if self._is_first(item):
|
if self._is_first(item, self.first_item):
|
||||||
self._first_node = node
|
self._first_node = node
|
||||||
else:
|
else:
|
||||||
if self._first_node is None:
|
if self._first_node is None:
|
||||||
|
|||||||
+54
-21
@@ -293,7 +293,7 @@ class Post:
|
|||||||
url = re.sub(r'([?&])se=\d+&?', r'\1', orig_url).rstrip('&')
|
url = re.sub(r'([?&])se=\d+&?', r'\1', orig_url).rstrip('&')
|
||||||
return url
|
return url
|
||||||
except (InstaloaderException, KeyError, IndexError) as err:
|
except (InstaloaderException, KeyError, IndexError) as err:
|
||||||
self._context.error('{} Unable to fetch high quality image version of {}.'.format(err, self))
|
self._context.error(f"Unable to fetch high quality image version of {self}: {err}")
|
||||||
return self._node["display_url"] if "display_url" in self._node else self._node["display_src"]
|
return self._node["display_url"] if "display_url" in self._node else self._node["display_src"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -357,8 +357,7 @@ class Post:
|
|||||||
orig_url = carousel_media[idx]['image_versions2']['candidates'][0]['url']
|
orig_url = carousel_media[idx]['image_versions2']['candidates'][0]['url']
|
||||||
display_url = re.sub(r'([?&])se=\d+&?', r'\1', orig_url).rstrip('&')
|
display_url = re.sub(r'([?&])se=\d+&?', r'\1', orig_url).rstrip('&')
|
||||||
except (InstaloaderException, KeyError, IndexError) as err:
|
except (InstaloaderException, KeyError, IndexError) as err:
|
||||||
self._context.error('{} Unable to fetch high quality image version of {}.'.format(
|
self._context.error(f"Unable to fetch high quality image version of {self}: {err}")
|
||||||
err, self))
|
|
||||||
yield PostSidecarNode(is_video=is_video, display_url=display_url,
|
yield PostSidecarNode(is_video=is_video, display_url=display_url,
|
||||||
video_url=node['video_url'] if is_video else None)
|
video_url=node['video_url'] if is_video else None)
|
||||||
|
|
||||||
@@ -953,11 +952,18 @@ class Profile:
|
|||||||
try:
|
try:
|
||||||
return self._iphone_struct['hd_profile_pic_url_info']['url']
|
return self._iphone_struct['hd_profile_pic_url_info']['url']
|
||||||
except (InstaloaderException, KeyError) as err:
|
except (InstaloaderException, KeyError) as err:
|
||||||
self._context.error('{} Unable to fetch high quality profile pic.'.format(err))
|
self._context.error(f"Unable to fetch high quality profile pic: {err}")
|
||||||
return self._metadata("profile_pic_url_hd")
|
return self._metadata("profile_pic_url_hd")
|
||||||
else:
|
else:
|
||||||
return self._metadata("profile_pic_url_hd")
|
return self._metadata("profile_pic_url_hd")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def profile_pic_url_no_iphone(self) -> str:
|
||||||
|
"""Return URL of lower-quality profile picture.
|
||||||
|
|
||||||
|
.. versionadded:: 4.9.3"""
|
||||||
|
return self._metadata("profile_pic_url_hd")
|
||||||
|
|
||||||
def get_profile_pic_url(self) -> str:
|
def get_profile_pic_url(self) -> str:
|
||||||
""".. deprecated:: 4.0.3
|
""".. deprecated:: 4.0.3
|
||||||
|
|
||||||
@@ -1033,17 +1039,8 @@ class Profile:
|
|||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _make_is_newest_checker() -> Callable[[Post], bool]:
|
def _make_is_newest_checker() -> Callable[[Post, Optional[Post]], bool]:
|
||||||
newest_date: Optional[datetime] = None
|
return lambda post, first: first is None or post.date_local > first.date_local
|
||||||
def is_newest(p: Post) -> bool:
|
|
||||||
nonlocal newest_date
|
|
||||||
post_date = p.date_local
|
|
||||||
if newest_date is None or post_date > newest_date:
|
|
||||||
newest_date = post_date
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
return is_newest
|
|
||||||
|
|
||||||
def get_followers(self) -> NodeIterator['Profile']:
|
def get_followers(self) -> NodeIterator['Profile']:
|
||||||
"""
|
"""
|
||||||
@@ -1172,8 +1169,14 @@ class StoryItem:
|
|||||||
if not self._context.is_logged_in:
|
if not self._context.is_logged_in:
|
||||||
raise LoginRequiredException("--login required to access iPhone media info endpoint.")
|
raise LoginRequiredException("--login required to access iPhone media info endpoint.")
|
||||||
if not self._iphone_struct_:
|
if not self._iphone_struct_:
|
||||||
data = self._context.get_iphone_json(path='api/v1/media/{}/info/'.format(self.mediaid), params={})
|
data = self._context.get_iphone_json(
|
||||||
self._iphone_struct_ = data['items'][0]
|
path='api/v1/feed/reels_media/?reel_ids={}'.format(self.owner_id), params={}
|
||||||
|
)
|
||||||
|
self._iphone_struct_ = {}
|
||||||
|
for item in data['reels'][str(self.owner_id)]['items']:
|
||||||
|
if item['pk'] == self.mediaid:
|
||||||
|
self._iphone_struct_ = item
|
||||||
|
break
|
||||||
return self._iphone_struct_
|
return self._iphone_struct_
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1237,7 +1240,7 @@ class StoryItem:
|
|||||||
url = re.sub(r'([?&])se=\d+&?', r'\1', orig_url).rstrip('&')
|
url = re.sub(r'([?&])se=\d+&?', r'\1', orig_url).rstrip('&')
|
||||||
return url
|
return url
|
||||||
except (InstaloaderException, KeyError, IndexError) as err:
|
except (InstaloaderException, KeyError, IndexError) as err:
|
||||||
self._context.error('{} Unable to fetch high quality image version of {}.'.format(err, self))
|
self._context.error(f"Unable to fetch high quality image version of {self}: {err}")
|
||||||
return self._node['display_resources'][-1]['src']
|
return self._node['display_resources'][-1]['src']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1312,6 +1315,7 @@ class Story:
|
|||||||
self._node = node
|
self._node = node
|
||||||
self._unique_id = None # type: Optional[str]
|
self._unique_id = None # type: Optional[str]
|
||||||
self._owner_profile = None # type: Optional[Profile]
|
self._owner_profile = None # type: Optional[Profile]
|
||||||
|
self._iphone_struct_ = None # type: Optional[Dict[str, Any]]
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Story by {} changed {:%Y-%m-%d_%H-%M-%S_UTC}>'.format(self.owner_username, self.latest_media_utc)
|
return '<Story by {} changed {:%Y-%m-%d_%H-%M-%S_UTC}>'.format(self.owner_username, self.latest_media_utc)
|
||||||
@@ -1382,9 +1386,23 @@ class Story:
|
|||||||
"""The story owner's ID."""
|
"""The story owner's ID."""
|
||||||
return self.owner_profile.userid
|
return self.owner_profile.userid
|
||||||
|
|
||||||
|
def _fetch_iphone_struct(self) -> None:
|
||||||
|
if self._context.iphone_support and self._context.is_logged_in and not self._iphone_struct_:
|
||||||
|
data = self._context.get_iphone_json(
|
||||||
|
path='api/v1/feed/reels_media/?reel_ids={}'.format(self.owner_id), params={}
|
||||||
|
)
|
||||||
|
self._iphone_struct_ = data['reels'][str(self.owner_id)]
|
||||||
|
|
||||||
def get_items(self) -> Iterator[StoryItem]:
|
def get_items(self) -> Iterator[StoryItem]:
|
||||||
"""Retrieve all items from a story."""
|
"""Retrieve all items from a story."""
|
||||||
yield from (StoryItem(self._context, item, self.owner_profile) for item in reversed(self._node['items']))
|
self._fetch_iphone_struct()
|
||||||
|
for item in reversed(self._node['items']):
|
||||||
|
if self._iphone_struct_ is not None:
|
||||||
|
for iphone_struct_item in self._iphone_struct_['items']:
|
||||||
|
if iphone_struct_item['pk'] == int(item['id']):
|
||||||
|
item['iphone_struct'] = iphone_struct_item
|
||||||
|
break
|
||||||
|
yield StoryItem(self._context, item, self.owner_profile)
|
||||||
|
|
||||||
|
|
||||||
class Highlight(Story):
|
class Highlight(Story):
|
||||||
@@ -1414,6 +1432,7 @@ class Highlight(Story):
|
|||||||
super().__init__(context, node)
|
super().__init__(context, node)
|
||||||
self._owner_profile = owner
|
self._owner_profile = owner
|
||||||
self._items = None # type: Optional[List[Dict[str, Any]]]
|
self._items = None # type: Optional[List[Dict[str, Any]]]
|
||||||
|
self._iphone_struct_ = None # type: Optional[Dict[str, Any]]
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Highlight by {}: {}>'.format(self.owner_username, self.title)
|
return '<Highlight by {}: {}>'.format(self.owner_username, self.title)
|
||||||
@@ -1452,6 +1471,13 @@ class Highlight(Story):
|
|||||||
"highlight_reel_ids": [str(self.unique_id)],
|
"highlight_reel_ids": [str(self.unique_id)],
|
||||||
"precomposed_overlay": False})['data']['reels_media'][0]['items']
|
"precomposed_overlay": False})['data']['reels_media'][0]['items']
|
||||||
|
|
||||||
|
def _fetch_iphone_struct(self) -> None:
|
||||||
|
if self._context.iphone_support and self._context.is_logged_in and not self._iphone_struct_:
|
||||||
|
data = self._context.get_iphone_json(
|
||||||
|
path='api/v1/feed/reels_media/?reel_ids=highlight:{}'.format(self.unique_id), params={}
|
||||||
|
)
|
||||||
|
self._iphone_struct_ = data['reels']['highlight:{}'.format(self.unique_id)]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def itemcount(self) -> int:
|
def itemcount(self) -> int:
|
||||||
"""Count of items associated with the :class:`Highlight` instance."""
|
"""Count of items associated with the :class:`Highlight` instance."""
|
||||||
@@ -1462,8 +1488,15 @@ class Highlight(Story):
|
|||||||
def get_items(self) -> Iterator[StoryItem]:
|
def get_items(self) -> Iterator[StoryItem]:
|
||||||
"""Retrieve all associated highlight items."""
|
"""Retrieve all associated highlight items."""
|
||||||
self._fetch_items()
|
self._fetch_items()
|
||||||
|
self._fetch_iphone_struct()
|
||||||
assert self._items is not None
|
assert self._items is not None
|
||||||
yield from (StoryItem(self._context, item, self.owner_profile) for item in self._items)
|
for item in self._items:
|
||||||
|
if self._iphone_struct_ is not None:
|
||||||
|
for iphone_struct_item in self._iphone_struct_['items']:
|
||||||
|
if iphone_struct_item['pk'] == int(item['id']):
|
||||||
|
item['iphone_struct'] = iphone_struct_item
|
||||||
|
break
|
||||||
|
yield StoryItem(self._context, item, self.owner_profile)
|
||||||
|
|
||||||
|
|
||||||
class Hashtag:
|
class Hashtag:
|
||||||
@@ -1517,7 +1550,7 @@ class Hashtag:
|
|||||||
|
|
||||||
def _obtain_metadata(self):
|
def _obtain_metadata(self):
|
||||||
if not self._has_full_metadata:
|
if not self._has_full_metadata:
|
||||||
self._node = self._query({"__a": 1})
|
self._node = self._query({"__a": 1, "__d": "dis"})
|
||||||
self._has_full_metadata = True
|
self._has_full_metadata = True
|
||||||
|
|
||||||
def _asdict(self):
|
def _asdict(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user