mirror of
https://github.com/instaloader/instaloader.git
synced 2026-10-16 13:48:03 -07:00
feat: add --reels flag for downloading Reels videos from profiles (#2355)
This commit is contained in:
+1
-1
@@ -53,7 +53,7 @@
|
|||||||
::
|
::
|
||||||
|
|
||||||
instaloader [--comments] [--geotags]
|
instaloader [--comments] [--geotags]
|
||||||
[--stories] [--highlights] [--tagged] [--igtv]
|
[--stories] [--highlights] [--tagged] [--reels] [--igtv]
|
||||||
[--login YOUR-USERNAME] [--fast-update]
|
[--login YOUR-USERNAME] [--fast-update]
|
||||||
profile | "#hashtag" | :stories | :feed | :saved
|
profile | "#hashtag" | :stories | :feed | :saved
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ Instaloader supports the following targets:
|
|||||||
- :option:`--tagged`
|
- :option:`--tagged`
|
||||||
to **download posts where the user is tagged**, and
|
to **download posts where the user is tagged**, and
|
||||||
|
|
||||||
|
- :option:`--reels`
|
||||||
|
to **download Reels videos**.
|
||||||
|
|
||||||
- :option:`--igtv`
|
- :option:`--igtv`
|
||||||
to **download IGTV videos**.
|
to **download IGTV videos**.
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,12 @@ What to Download of each Profile
|
|||||||
|
|
||||||
.. versionadded:: 4.1
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
.. option:: --reels
|
||||||
|
|
||||||
|
Also download Reels videos.
|
||||||
|
|
||||||
|
.. versionadded:: 4.14
|
||||||
|
|
||||||
.. option:: --igtv
|
.. option:: --igtv
|
||||||
|
|
||||||
Also download IGTV videos.
|
Also download IGTV videos.
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ See :ref:`install` for more options on how to install Instaloader.
|
|||||||
::
|
::
|
||||||
|
|
||||||
instaloader [--comments] [--geotags]
|
instaloader [--comments] [--geotags]
|
||||||
[--stories] [--highlights] [--tagged] [--igtv]
|
[--stories] [--highlights] [--tagged] [--reels] [--igtv]
|
||||||
[--login YOUR-USERNAME] [--fast-update]
|
[--login YOUR-USERNAME] [--fast-update]
|
||||||
profile | "#hashtag" | %location_id |
|
profile | "#hashtag" | %location_id |
|
||||||
:stories | :feed | :saved
|
:stories | :feed | :saved
|
||||||
|
|||||||
+35
-13
@@ -37,7 +37,7 @@ def usage_string():
|
|||||||
argv0 = "instaloader" if argv0 == "__main__.py" else argv0
|
argv0 = "instaloader" if argv0 == "__main__.py" else argv0
|
||||||
return """
|
return """
|
||||||
{0} [--comments] [--geotags]
|
{0} [--comments] [--geotags]
|
||||||
{2:{1}} [--stories] [--highlights] [--tagged] [--igtv]
|
{2:{1}} [--stories] [--highlights] [--tagged] [--reels] [--igtv]
|
||||||
{2:{1}} [--login YOUR-USERNAME] [--fast-update]
|
{2:{1}} [--login YOUR-USERNAME] [--fast-update]
|
||||||
{2:{1}} profile | "#hashtag" | %%location_id | :stories | :feed | :saved
|
{2:{1}} profile | "#hashtag" | %%location_id | :stories | :feed | :saved
|
||||||
{0} --help""".format(argv0, len(argv0), '')
|
{0} --help""".format(argv0, len(argv0), '')
|
||||||
@@ -139,6 +139,7 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
|||||||
download_stories: bool = False,
|
download_stories: bool = False,
|
||||||
download_highlights: bool = False,
|
download_highlights: bool = False,
|
||||||
download_tagged: bool = False,
|
download_tagged: bool = False,
|
||||||
|
download_reels: bool = False,
|
||||||
download_igtv: bool = False,
|
download_igtv: bool = False,
|
||||||
fast_update: bool = False,
|
fast_update: bool = False,
|
||||||
latest_stamps_file: Optional[str] = None,
|
latest_stamps_file: Optional[str] = None,
|
||||||
@@ -258,11 +259,13 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
|||||||
instaloader.download_saved_posts(fast_update=fast_update, max_count=max_count,
|
instaloader.download_saved_posts(fast_update=fast_update, max_count=max_count,
|
||||||
post_filter=post_filter)
|
post_filter=post_filter)
|
||||||
elif re.match(r"^[A-Za-z0-9._]+$", target):
|
elif re.match(r"^[A-Za-z0-9._]+$", target):
|
||||||
|
download_profile_content = download_posts or download_tagged or download_reels or download_igtv
|
||||||
try:
|
try:
|
||||||
profile = instaloader.check_profile_id(target, latest_stamps)
|
profile = instaloader.check_profile_id(target, latest_stamps)
|
||||||
if instaloader.context.is_logged_in and profile.has_blocked_viewer:
|
if instaloader.context.is_logged_in and profile.has_blocked_viewer:
|
||||||
if download_profile_pic or ((download_posts or download_tagged or download_igtv)
|
if download_profile_pic or (
|
||||||
and not profile.is_private):
|
download_profile_content and not profile.is_private
|
||||||
|
):
|
||||||
raise ProfileNotExistsException("{} blocked you; But we download her anonymously."
|
raise ProfileNotExistsException("{} blocked you; But we download her anonymously."
|
||||||
.format(target))
|
.format(target))
|
||||||
else:
|
else:
|
||||||
@@ -272,8 +275,7 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
|||||||
except ProfileNotExistsException as err:
|
except ProfileNotExistsException as err:
|
||||||
# Not only our profile.has_blocked_viewer condition raises ProfileNotExistsException,
|
# Not only our profile.has_blocked_viewer condition raises ProfileNotExistsException,
|
||||||
# check_profile_id() also does, since access to blocked profile may be responded with 404.
|
# check_profile_id() also does, since access to blocked profile may be responded with 404.
|
||||||
if instaloader.context.is_logged_in and (download_profile_pic or download_posts or
|
if instaloader.context.is_logged_in and (download_profile_pic or download_profile_content):
|
||||||
download_tagged or download_igtv):
|
|
||||||
instaloader.context.log(err)
|
instaloader.context.log(err)
|
||||||
instaloader.context.log("Trying again anonymously, helps in case you are just blocked.")
|
instaloader.context.log("Trying again anonymously, helps in case you are just blocked.")
|
||||||
with instaloader.anonymous_copy() as anonymous_loader:
|
with instaloader.anonymous_copy() as anonymous_loader:
|
||||||
@@ -297,18 +299,35 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
|||||||
if instaloader.context.iphone_support and profiles and (download_profile_pic or download_posts) and \
|
if instaloader.context.iphone_support and profiles and (download_profile_pic or download_posts) and \
|
||||||
not instaloader.context.is_logged_in:
|
not instaloader.context.is_logged_in:
|
||||||
instaloader.context.log("Hint: Login to download higher-quality versions of pictures.")
|
instaloader.context.log("Hint: Login to download higher-quality versions of pictures.")
|
||||||
instaloader.download_profiles(profiles,
|
instaloader.download_profiles(
|
||||||
download_profile_pic, download_posts, download_tagged, download_igtv,
|
profiles,
|
||||||
download_highlights, download_stories,
|
download_profile_pic,
|
||||||
fast_update, post_filter, storyitem_filter, latest_stamps=latest_stamps)
|
download_posts,
|
||||||
|
download_tagged,
|
||||||
|
download_igtv,
|
||||||
|
download_highlights,
|
||||||
|
download_stories,
|
||||||
|
fast_update,
|
||||||
|
post_filter,
|
||||||
|
storyitem_filter,
|
||||||
|
latest_stamps=latest_stamps,
|
||||||
|
reels=download_reels,
|
||||||
|
)
|
||||||
if anonymous_retry_profiles:
|
if anonymous_retry_profiles:
|
||||||
instaloader.context.log("Downloading anonymously: {}"
|
instaloader.context.log("Downloading anonymously: {}"
|
||||||
.format(' '.join([p.username for p in anonymous_retry_profiles])))
|
.format(' '.join([p.username for p in anonymous_retry_profiles])))
|
||||||
with instaloader.anonymous_copy() as anonymous_loader:
|
with instaloader.anonymous_copy() as anonymous_loader:
|
||||||
anonymous_loader.download_profiles(anonymous_retry_profiles,
|
anonymous_loader.download_profiles(
|
||||||
download_profile_pic, download_posts, download_tagged, download_igtv,
|
anonymous_retry_profiles,
|
||||||
fast_update=fast_update, post_filter=post_filter,
|
download_profile_pic,
|
||||||
latest_stamps=latest_stamps)
|
download_posts,
|
||||||
|
download_tagged,
|
||||||
|
download_igtv,
|
||||||
|
fast_update=fast_update,
|
||||||
|
post_filter=post_filter,
|
||||||
|
latest_stamps=latest_stamps,
|
||||||
|
reels=download_reels
|
||||||
|
)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nInterrupted by user.", file=sys.stderr)
|
print("\nInterrupted by user.", file=sys.stderr)
|
||||||
exit_code = ExitCode.USER_ABORTED
|
exit_code = ExitCode.USER_ABORTED
|
||||||
@@ -409,6 +428,8 @@ def main():
|
|||||||
help='Also download highlights of each profile that is downloaded. Requires login.')
|
help='Also download highlights of each profile that is downloaded. Requires login.')
|
||||||
g_prof.add_argument('--tagged', action='store_true',
|
g_prof.add_argument('--tagged', action='store_true',
|
||||||
help='Also download posts where each profile is tagged.')
|
help='Also download posts where each profile is tagged.')
|
||||||
|
g_prof.add_argument('--reels', action='store_true',
|
||||||
|
help='Also download Reels videos.')
|
||||||
g_prof.add_argument('--igtv', action='store_true',
|
g_prof.add_argument('--igtv', action='store_true',
|
||||||
help='Also download IGTV videos.')
|
help='Also download IGTV videos.')
|
||||||
|
|
||||||
@@ -570,6 +591,7 @@ def main():
|
|||||||
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_reels=args.reels,
|
||||||
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,
|
||||||
|
|||||||
@@ -1281,6 +1281,31 @@ class Instaloader:
|
|||||||
if latest_stamps is not None and tagged_posts.first_item is not None:
|
if latest_stamps is not None and tagged_posts.first_item is not None:
|
||||||
latest_stamps.set_last_tagged_timestamp(profile.username, tagged_posts.first_item.date_local)
|
latest_stamps.set_last_tagged_timestamp(profile.username, tagged_posts.first_item.date_local)
|
||||||
|
|
||||||
|
def download_reels(self, profile: Profile, fast_update: bool = False,
|
||||||
|
post_filter: Optional[Callable[[Post], bool]] = None,
|
||||||
|
latest_stamps: Optional[LatestStamps] = None) -> None:
|
||||||
|
"""Download reels videos of a profile.
|
||||||
|
|
||||||
|
.. versionadded:: 4.14.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.context.log("Retrieving reels videos for profile {}.".format(profile.username))
|
||||||
|
posts_takewhile: Optional[Callable[[Post], bool]] = None
|
||||||
|
if latest_stamps is not None:
|
||||||
|
last_scraped = latest_stamps.get_last_reels_timestamp(profile.username)
|
||||||
|
posts_takewhile = lambda p: p.date_local > last_scraped
|
||||||
|
reels = profile.get_reels()
|
||||||
|
self.posts_download_loop(
|
||||||
|
reels,
|
||||||
|
profile.username,
|
||||||
|
fast_update,
|
||||||
|
post_filter,
|
||||||
|
owner_profile=profile,
|
||||||
|
takewhile=posts_takewhile,
|
||||||
|
)
|
||||||
|
if latest_stamps is not None and reels.first_item is not None:
|
||||||
|
latest_stamps.set_last_reels_timestamp(profile.username, reels.first_item.date_local)
|
||||||
|
|
||||||
def download_igtv(self, profile: Profile, fast_update: bool = False,
|
def download_igtv(self, profile: Profile, fast_update: bool = False,
|
||||||
post_filter: Optional[Callable[[Post], bool]] = None,
|
post_filter: Optional[Callable[[Post], bool]] = None,
|
||||||
latest_stamps: Optional[LatestStamps] = None) -> None:
|
latest_stamps: Optional[LatestStamps] = None) -> None:
|
||||||
@@ -1411,7 +1436,8 @@ class Instaloader:
|
|||||||
storyitem_filter: Optional[Callable[[Post], bool]] = None,
|
storyitem_filter: Optional[Callable[[Post], bool]] = None,
|
||||||
raise_errors: bool = False,
|
raise_errors: bool = False,
|
||||||
latest_stamps: Optional[LatestStamps] = None,
|
latest_stamps: Optional[LatestStamps] = None,
|
||||||
max_count: Optional[int] = None):
|
max_count: Optional[int] = None,
|
||||||
|
reels: bool = False):
|
||||||
"""High-level method to download set of profiles.
|
"""High-level method to download set of profiles.
|
||||||
|
|
||||||
:param profiles: Set of profiles to download.
|
:param profiles: Set of profiles to download.
|
||||||
@@ -1429,6 +1455,7 @@ class Instaloader:
|
|||||||
catched and printed with :meth:`InstaloaderContext.error_catcher`.
|
catched and printed with :meth:`InstaloaderContext.error_catcher`.
|
||||||
:param latest_stamps: :option:`--latest-stamps`.
|
:param latest_stamps: :option:`--latest-stamps`.
|
||||||
:param max_count: Maximum count of posts to download.
|
:param max_count: Maximum count of posts to download.
|
||||||
|
:param reels: :option:`--reels`.
|
||||||
|
|
||||||
.. versionadded:: 4.1
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
@@ -1440,6 +1467,9 @@ class Instaloader:
|
|||||||
|
|
||||||
.. versionchanged:: 4.13
|
.. versionchanged:: 4.13
|
||||||
Add `max_count` parameter.
|
Add `max_count` parameter.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.14
|
||||||
|
Add `reels` parameter.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
@@ -1483,6 +1513,12 @@ class Instaloader:
|
|||||||
self.download_tagged(profile, fast_update=fast_update, post_filter=post_filter,
|
self.download_tagged(profile, fast_update=fast_update, post_filter=post_filter,
|
||||||
latest_stamps=latest_stamps)
|
latest_stamps=latest_stamps)
|
||||||
|
|
||||||
|
# Download reels, if requested
|
||||||
|
if reels:
|
||||||
|
with self.context.error_catcher('Download reels of {}'.format(profile_name)):
|
||||||
|
self.download_reels(profile, fast_update=fast_update, post_filter=post_filter,
|
||||||
|
latest_stamps=latest_stamps)
|
||||||
|
|
||||||
# Download IGTV, if requested
|
# Download IGTV, if requested
|
||||||
if igtv:
|
if igtv:
|
||||||
with self.context.error_catcher('Download IGTV of {}'.format(profile_name)):
|
with self.context.error_catcher('Download IGTV of {}'.format(profile_name)):
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class LatestStamps:
|
|||||||
POST_TIMESTAMP = 'post-timestamp'
|
POST_TIMESTAMP = 'post-timestamp'
|
||||||
TAGGED_TIMESTAMP = 'tagged-timestamp'
|
TAGGED_TIMESTAMP = 'tagged-timestamp'
|
||||||
IGTV_TIMESTAMP = 'igtv-timestamp'
|
IGTV_TIMESTAMP = 'igtv-timestamp'
|
||||||
|
REELS_TIMESTAMP = 'reels-timestamp'
|
||||||
STORY_TIMESTAMP = 'story-timestamp'
|
STORY_TIMESTAMP = 'story-timestamp'
|
||||||
ISO_FORMAT = '%Y-%m-%dT%H:%M:%S.%f%z'
|
ISO_FORMAT = '%Y-%m-%dT%H:%M:%S.%f%z'
|
||||||
|
|
||||||
@@ -87,6 +88,14 @@ class LatestStamps:
|
|||||||
"""Sets timestamp of last download of a profile's tagged posts."""
|
"""Sets timestamp of last download of a profile's tagged posts."""
|
||||||
self._set_timestamp(profile_name, self.TAGGED_TIMESTAMP, timestamp)
|
self._set_timestamp(profile_name, self.TAGGED_TIMESTAMP, timestamp)
|
||||||
|
|
||||||
|
def get_last_reels_timestamp(self, profile_name: str) -> datetime:
|
||||||
|
"""Returns timestamp of last download of a profile's reels posts."""
|
||||||
|
return self._get_timestamp(profile_name, self.REELS_TIMESTAMP)
|
||||||
|
|
||||||
|
def set_last_reels_timestamp(self, profile_name: str, timestamp: datetime):
|
||||||
|
"""Sets timestamp of last download of a profile's reels posts."""
|
||||||
|
self._set_timestamp(profile_name, self.REELS_TIMESTAMP, timestamp)
|
||||||
|
|
||||||
def get_last_igtv_timestamp(self, profile_name: str) -> datetime:
|
def get_last_igtv_timestamp(self, profile_name: str) -> datetime:
|
||||||
"""Returns timestamp of last download of a profile's igtv posts."""
|
"""Returns timestamp of last download of a profile's igtv posts."""
|
||||||
return self._get_timestamp(profile_name, self.IGTV_TIMESTAMP)
|
return self._get_timestamp(profile_name, self.IGTV_TIMESTAMP)
|
||||||
|
|||||||
@@ -1232,6 +1232,30 @@ class Profile:
|
|||||||
is_first=Profile._make_is_newest_checker()
|
is_first=Profile._make_is_newest_checker()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_reels(self) -> NodeIterator[Post]:
|
||||||
|
"""Retrieve all reels from a profile.
|
||||||
|
|
||||||
|
:rtype: NodeIterator[Post]
|
||||||
|
|
||||||
|
.. versionadded:: 4.14.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._obtain_metadata()
|
||||||
|
return NodeIterator(
|
||||||
|
context = self._context,
|
||||||
|
edge_extractor = lambda d: d['data']['xdt_api__v1__clips__user__connection_v2'],
|
||||||
|
# Reels post info is incomplete relative to regular posts so we create a Post from the shortcode
|
||||||
|
# and fetch the additional metadata with an additional API request per Reel
|
||||||
|
node_wrapper = lambda n: Post.from_shortcode(context=self._context, shortcode=n["media"]["code"]),
|
||||||
|
query_variables = {'data': {
|
||||||
|
'page_size': 12, 'include_feed_video': True, "target_user_id": str(self.userid)}},
|
||||||
|
query_referer = 'https://www.instagram.com/{0}/'.format(self.username),
|
||||||
|
is_first = Profile._make_is_newest_checker(),
|
||||||
|
# fb_api_req_friendly_name=PolarisProfileReelsTabContentQuery_connection
|
||||||
|
doc_id = '7845543455542541',
|
||||||
|
query_hash = None,
|
||||||
|
)
|
||||||
|
|
||||||
def get_igtv_posts(self) -> NodeIterator[Post]:
|
def get_igtv_posts(self) -> NodeIterator[Post]:
|
||||||
"""Retrieve all IGTV posts.
|
"""Retrieve all IGTV posts.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user