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:
parent
66f69b5c21
commit
b1b90f8abf
22
README.rst
22
README.rst
@ -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
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user