target :stories; flags --stories & --stories-only

This allows to invoke the new download_stories() function contributed
in #28 by command line.
This commit is contained in:
Alexander Graf 2017-07-29 17:51:39 +02:00
parent 66f69b5c21
commit b1b90f8abf
2 changed files with 67 additions and 17 deletions

View File

@ -68,6 +68,12 @@ in your temporary directory, which will be reused later when ``--login`` is give
you can download private profiles **non-interactively** when you already you can download private profiles **non-interactively** when you already
have a valid session cookie file. have a valid session cookie file.
Besides downloading private profiles, being logged in allows to
**download stories**:
::
instaloader --login=your_username --stories profile [profile ...]
You may also download You may also download
**the most recent pictures by hashtag**: **the most recent pictures by hashtag**:
@ -102,6 +108,12 @@ or to **download all pictures from your feed**:
instaloader --login=your_username :feed-all instaloader --login=your_username :feed-all
**Download all stories** from the profiles you follow:
::
instaloader --login=your_username --filename-pattern={date}_{profile} :stories
Advanced Options Advanced Options
---------------- ----------------
@ -120,6 +132,11 @@ captions and the current **profile picture**. If an already-downloaded profile
has been renamed, Instaloader automatically **finds it by its unique ID** and has been renamed, Instaloader automatically **finds it by its unique ID** and
renames the folder likewise. renames the folder likewise.
Instead of a *profile* or a *#hashtag*, the special targets
``:feed-all`` (pictures from your feed),
``:feed-liked`` (pictures from your feed which you liked), and
``:stories`` (stories of your followees) can be specified.
--profile-pic-only Only download profile picture. --profile-pic-only Only download profile picture.
--skip-videos Do not download videos. --skip-videos Do not download videos.
--geotags **Download geotags** when available. Geotags are stored as --geotags **Download geotags** when available. Geotags are stored as
@ -130,6 +147,11 @@ renames the folder likewise.
--comments Download and update comments for each post. This --comments Download and update comments for each post. This
requires an additional request to the Instagram server requires an additional request to the Instagram server
for each post, which is why it is disabled by default. for each post, which is why it is disabled by default.
--stories Also **download stories** of each profile that is
downloaded. Requires ``--login``.
--stories-only Rather than downloading regular posts of each
specified profile, only download stories.
Requires ``--login``.
When to Stop Downloading When to Stop Downloading
^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -17,7 +17,7 @@ from argparse import ArgumentParser
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from datetime import datetime from datetime import datetime
from io import BytesIO from io import BytesIO
from typing import Any, Callable, Dict, List, Optional from typing import Any, Callable, Dict, List, Optional, Tuple
import requests import requests
import requests.utils import requests.utils
@ -629,7 +629,8 @@ class Instaloader:
def download_stories(self, def download_stories(self,
userids: Optional[List[int]] = None, userids: Optional[List[int]] = None,
download_videos: bool = True, download_videos: bool = True,
fast_update: bool = False) -> None: fast_update: bool = False,
filename_target: str = ':stories') -> None:
""" """
Download available stories from user followees or all stories of users whose ID are given. Download available stories from user followees or all stories of users whose ID are given.
Does not mark stories as seen. Does not mark stories as seen.
@ -638,6 +639,7 @@ class Instaloader:
:param userids: List of user IDs to be processed in terms of downloading their stories :param userids: List of user IDs to be processed in terms of downloading their stories
:param download_videos: True, if videos should be downloaded :param download_videos: True, if videos should be downloaded
:param fast_update: If true, abort when first already-downloaded picture is encountered :param fast_update: If true, abort when first already-downloaded picture is encountered
:param filename_target: Replacement for {target} in dirname_pattern and filename_pattern
""" """
if self.username is None: if self.username is None:
@ -675,6 +677,7 @@ class Instaloader:
if "items" not in user_stories: if "items" not in user_stories:
continue continue
name = user_stories["user"]["username"].lower() name = user_stories["user"]["username"].lower()
self._log("Retrieving stories from profile {}.".format(name))
totalcount = len(user_stories["items"]) if "items" in user_stories else 0 totalcount = len(user_stories["items"]) if "items" in user_stories else 0
count = 1 count = 1
for item in user_stories["items"]: for item in user_stories["items"]:
@ -692,8 +695,8 @@ class Instaloader:
date_float /= 1000 date_float /= 1000
date = datetime.fromtimestamp(date_float) date = datetime.fromtimestamp(date_float)
dirname = self.dirname_pattern.format(profile=name, target=':stories') dirname = self.dirname_pattern.format(profile=name, target=filename_target)
filename = dirname + '/' + self.filename_pattern.format(profile=name, target=':stories', filename = dirname + '/' + self.filename_pattern.format(profile=name, target=filename_target,
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)
@ -827,10 +830,12 @@ class Instaloader:
else: else:
break break
def check_id(self, profile: str, json_data: Dict[str, Any]) -> str: def check_id(self, profile: str, json_data: Dict[str, Any]) -> Tuple[str, int]:
""" """
Consult locally stored ID of profile with given name, check whether ID matches and whether name Consult locally stored ID of profile with given name, check whether ID matches and whether name
has changed and return current name of the profile, and store ID of profile. has changed and return current name of the profile, and store ID of profile.
:return: current profile name, profile id
""" """
profile_exists = "ProfilePage" in json_data["entry_data"] profile_exists = "ProfilePage" in json_data["entry_data"]
if ((format_string_contains_key(self.dirname_pattern, 'profile') or if ((format_string_contains_key(self.dirname_pattern, 'profile') or
@ -859,8 +864,8 @@ class Instaloader:
else: else:
os.rename('{0}/{1}_id'.format(self.dirname_pattern.format(), profile.lower()), os.rename('{0}/{1}_id'.format(self.dirname_pattern.format(), profile.lower()),
'{0}/{1}_id'.format(self.dirname_pattern.format(), newname.lower())) '{0}/{1}_id'.format(self.dirname_pattern.format(), newname.lower()))
return newname return newname, profile_id
return profile return profile, profile_id
except FileNotFoundError: except FileNotFoundError:
pass pass
if profile_exists: if profile_exists:
@ -870,18 +875,19 @@ class Instaloader:
profile_id = json_data['entry_data']['ProfilePage'][0]['user']['id'] profile_id = json_data['entry_data']['ProfilePage'][0]['user']['id']
text_file.write(profile_id + "\n") text_file.write(profile_id + "\n")
self._log("Stored ID {0} for profile {1}.".format(profile_id, profile)) self._log("Stored ID {0} for profile {1}.".format(profile_id, profile))
return profile return profile, profile_id
raise ProfileNotExistsException("Profile {0} does not exist.".format(profile)) raise ProfileNotExistsException("Profile {0} does not exist.".format(profile))
def download(self, name: str, def download(self, name: str,
profile_pic_only: bool = False, download_videos: bool = True, geotags: bool = False, profile_pic_only: bool = False, download_videos: bool = True, geotags: bool = False,
download_comments: bool = False, fast_update: bool = False) -> None: download_comments: bool = False, fast_update: bool = False,
download_stories: bool = False, download_stories_only: bool = False) -> None:
"""Download one profile""" """Download one profile"""
# Get profile main page json # Get profile main page json
data = self.get_json(name) data = self.get_json(name)
# check if profile does exist or name has changed since last download # check if profile does exist or name has changed since last download
# and update name and json data if necessary # and update name and json data if necessary
name_updated = self.check_id(name, data) name_updated, profile_id = self.check_id(name, data)
if name_updated != name: if name_updated != name:
name = name_updated name = name_updated
data = self.get_json(name) data = self.get_json(name)
@ -896,14 +902,20 @@ class Instaloader:
if not data["entry_data"]["ProfilePage"][0]["user"]["followed_by_viewer"]: if not data["entry_data"]["ProfilePage"][0]["user"]["followed_by_viewer"]:
raise PrivateProfileNotFollowedException("Profile %s: private but not followed." % name) raise PrivateProfileNotFollowedException("Profile %s: private but not followed." % name)
else: else:
if data["config"]["viewer"] is not None: if data["config"]["viewer"] is not None and not (download_stories or download_stories_only):
self._log("profile %s could also be downloaded anonymously." % name) self._log("profile %s could also be downloaded anonymously." % name)
if download_stories or download_stories_only:
self.download_stories(userids=[profile_id], filename_target=name,
download_videos=download_videos, fast_update=fast_update)
if download_stories_only:
return
if ("nodes" not in data["entry_data"]["ProfilePage"][0]["user"]["media"] or if ("nodes" not in data["entry_data"]["ProfilePage"][0]["user"]["media"] or
not data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"]) \ not data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"]) \
and not profile_pic_only: and not profile_pic_only:
raise ProfileHasNoPicsException("Profile %s: no pics found." % name) raise ProfileHasNoPicsException("Profile %s: no pics found." % name)
# Iterate over pictures and download them # Iterate over pictures and download them
self._log("Retrieving posts from profile {}.".format(name))
def get_last_id(data): def get_last_id(data):
if data["entry_data"] and data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"]: if data["entry_data"] and data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"]:
return data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"][-1]["id"] return data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"][-1]["id"]
@ -940,7 +952,8 @@ class Instaloader:
sessionfile: Optional[str] = None, max_count: Optional[int] = None, sessionfile: Optional[str] = None, max_count: Optional[int] = None,
profile_pic_only: bool = False, download_videos: bool = True, geotags: bool = False, profile_pic_only: bool = False, download_videos: bool = True, geotags: bool = False,
download_comments: bool = False, download_comments: bool = False,
fast_update: bool = False) -> None: fast_update: bool = False,
stories: bool = False, stories_only: bool = False) -> None:
"""Download set of profiles and handle sessions""" """Download set of profiles and handle sessions"""
# Login, if desired # Login, if desired
if username is not None: if username is not None:
@ -994,6 +1007,11 @@ class Instaloader:
download_comments=download_comments) download_comments=download_comments)
else: else:
print("--login=USERNAME required to download {}.".format(pentry), file=sys.stderr) print("--login=USERNAME required to download {}.".format(pentry), file=sys.stderr)
elif pentry == ":stories":
if username is not None:
self.download_stories(download_videos=download_videos, fast_update=fast_update)
else:
print("--login=USERNAME required to download {}.".format(pentry), file=sys.stderr)
else: else:
targets.add(pentry) targets.add(pentry)
if len(targets) > 1: if len(targets) > 1:
@ -1003,7 +1021,7 @@ class Instaloader:
try: try:
try: try:
self.download(target, profile_pic_only, download_videos, self.download(target, profile_pic_only, download_videos,
geotags, download_comments, fast_update) geotags, download_comments, fast_update, stories, stories_only)
except ProfileNotExistsException as err: except ProfileNotExistsException as err:
if username is not None: if username is not None:
self._log(err) self._log(err)
@ -1041,9 +1059,9 @@ def main():
g_what.add_argument('profile', nargs='*', metavar='profile|#hashtag', g_what.add_argument('profile', nargs='*', metavar='profile|#hashtag',
help='Name of profile or #hashtag to download. ' help='Name of profile or #hashtag to download. '
'Alternatively, if --login is given: @<profile> to download all followees of ' 'Alternatively, if --login is given: @<profile> to download all followees of '
'<profile>; or the special targets :feed-all or :feed-liked to ' '<profile>; the special targets :feed-all or :feed-liked to '
'download pictures from your feed (using ' 'download pictures from your feed; or :stories to download the stories of your '
'--fast-update is recommended).') 'followees.')
g_what.add_argument('-P', '--profile-pic-only', action='store_true', g_what.add_argument('-P', '--profile-pic-only', action='store_true',
help='Only download profile picture.') help='Only download profile picture.')
g_what.add_argument('-V', '--skip-videos', action='store_true', g_what.add_argument('-V', '--skip-videos', action='store_true',
@ -1057,6 +1075,11 @@ def main():
help='Download and update comments for each post. ' help='Download and update comments for each post. '
'This requires an additional request to the Instagram ' 'This requires an additional request to the Instagram '
'server for each post, which is why it is disabled by default.') 'server for each post, which is why it is disabled by default.')
g_what.add_argument('-s', '--stories', action='store_true',
help='Also download stories of each profile that is downloaded. Requires --login.')
g_what.add_argument('--stories-only', action='store_true',
help='Rather than downloading regular posts of each specified profile, only download '
'stories. Requires --login.')
g_stop = parser.add_argument_group('When to Stop Downloading', g_stop = parser.add_argument_group('When to Stop Downloading',
'If none of these options are given, Instaloader goes through all pictures ' 'If none of these options are given, Instaloader goes through all pictures '
@ -1113,13 +1136,18 @@ def main():
args = parser.parse_args() args = parser.parse_args()
try: try:
if args.login is None and (args.stories or args.stories_only):
print("--login=USERNAME required to download stories.", file=sys.stderr)
args.stories = False
if args.stories_only:
raise SystemExit(1)
loader = Instaloader(sleep=not args.no_sleep, quiet=args.quiet, shorter_output=args.shorter_output, loader = Instaloader(sleep=not args.no_sleep, quiet=args.quiet, shorter_output=args.shorter_output,
user_agent=args.user_agent, user_agent=args.user_agent,
dirname_pattern=args.dirname_pattern, filename_pattern=args.filename_pattern) dirname_pattern=args.dirname_pattern, filename_pattern=args.filename_pattern)
loader.download_profiles(args.profile, args.login, args.password, args.sessionfile, loader.download_profiles(args.profile, args.login, args.password, args.sessionfile,
int(args.count) if args.count is not None else None, int(args.count) if args.count is not None else None,
args.profile_pic_only, not args.skip_videos, args.geotags, args.comments, args.profile_pic_only, not args.skip_videos, args.geotags, args.comments,
args.fast_update) args.fast_update, args.stories, args.stories_only)
except InstaloaderException as err: except InstaloaderException as err:
raise SystemExit("Fatal error: %s" % err) raise SystemExit("Fatal error: %s" % err)