Compare commits

..
18 Commits
Author SHA1 Message Date
André Koch-Kramer 52492456ed Release of version 1.3 2017-04-22 18:01:54 +02:00
Alexander Graf e0924e8d08 Fix --geotags 2017-04-22 17:54:21 +02:00
Alexander Graf 655dbb552d Further clarify meaning of --count 2017-04-22 17:34:49 +02:00
André Koch-Kramer fdb8e94c64 Fixed ":feed-liked" functionality 2017-04-22 17:26:48 +02:00
Alexander Graf b3c83f420c --count to limit posts at #hashtag and :feed-* 2017-04-22 17:21:02 +02:00
Alexander Graf df00e2b03a Minor enhancements to README 2017-04-22 12:11:56 +02:00
Alexander Graf b60f0b4836 README: Document all of Instaloader's features 2017-04-22 11:58:59 +02:00
Alexander Graf 35e71192c5 travis: Enable more Pylint inspections 2017-04-22 11:58:12 +02:00
André Koch-Kramer 2106c2d5f6 Satisfy pylint after their update 2017-04-22 11:13:10 +02:00
André Koch-Kramer 0e943189e5 Parse graphql structure for sidecars 2017-04-22 10:50:12 +02:00
André Koch-Kramer 8e77a1c125 Fixed download_node() 2017-04-22 01:39:52 +02:00
André Koch-Kramer 361445519a Adapt new graphql layout used by Instagram
Fixes #19.
2017-04-21 18:10:41 +02:00
Alexander Graf 6b345e1f52 Adapt video downloading to new format
This should the video downloading issue reported at #18.
2017-04-20 09:17:59 +02:00
Alexander Graf c841bca81b Silence new pylint warnings 2017-04-20 09:17:32 +02:00
Alexander Graf 3e1360160d Download pictures with #hashtag
Instaloader is now capable of downloading all pictures associated with
one #hashtag with:
instaloader #hashtag

This implements the feature requested with #18.
2017-04-17 12:16:22 +02:00
Alexander Graf a7d1c5bbb0 Make check_id() exceptions Non-fatal
The check_id() step, including the get_username_by_id(), which is used
to determine whether a profile's account name has changed since the last
download using it's unique id when Instaloader is operating logged-in,
is actually optional and should not cause termination in any case.
2017-04-10 21:05:58 +02:00
Alexander Graf dc748a0541 Download all pictures of Sidecar nodes 2017-03-25 21:08:54 +01:00
Alexander Graf 72c647829a Don't fail if --sessionfile does not contain '/' 2017-03-21 14:58:13 +01:00
4 changed files with 226 additions and 68 deletions
+1 -1
View File
@@ -9,4 +9,4 @@ python:
install:
- pip install pylint requests
script:
- python3 -m pylint -r n -d bad-whitespace,bad-continuation,missing-docstring,multiple-imports,too-many-arguments,locally-disabled,line-too-long,invalid-sequence-index instaloader
- python3 -m pylint -r n -d bad-whitespace,bad-continuation,missing-docstring,multiple-imports,too-many-arguments,locally-disabled,line-too-long instaloader
+60 -9
View File
@@ -1,6 +1,8 @@
Instaloader
===========
Tool to automatically download pictures (or videos) of given profiles
from Instagram.
Installation
------------
@@ -26,6 +28,8 @@ Alternatively, to get the most current version of Instaloader from our
pip3 install git+https://github.com/Thammus/instaloader
(pass ``--upgrade`` to upgrade if Instaloader is already installed)
Instaloader requires
`requests <https://pypi.python.org/pypi/requests>`__, which
will be installed automatically, if it is not already installed.
@@ -33,7 +37,8 @@ will be installed automatically, if it is not already installed.
How to automatically download pictures from Instagram
-----------------------------------------------------
To download a set of profiles, do
To **download all pictures and videos of a profile**, as well as the
**profile picture**, do
::
@@ -42,13 +47,13 @@ To download a set of profiles, do
where ``profile`` is the name of a profile you want to download. Instead
of only one profile, you may also specify a list of profiles.
To later update your local copy of that profile, you may run
To later **update your local copy** of that profiles, you may run
::
instaloader --fast-update profile [profile ...]
When ``--fast-update`` is given, Instaloader terminates when arriving at
When ``--fast-update`` is given, Instaloader stops when arriving at
the first already-downloaded picture.
Instaloader can also be used to **download private profiles**. To do so,
@@ -59,10 +64,17 @@ invoke it with
instaloader --login=your_username profile [profile ...]
When invoked like this, it also **stores the session cookies** in a file
in ``/tmp``, which will be reused later when ``--login`` is given. So
in your temporary directory, which will be reused later when ``--login`` is given. So
you can download private profiles **non-interactively** when you already
have a valid session cookie file.
You may also download
**the most recent pictures by hashtag**:
::
instaloader "#hashtag"
If you want to **download all followees of a given profile**, call
::
@@ -75,17 +87,49 @@ To **download all the pictures from your feed which you have liked**, call
instaloader --login=your_username :feed-liked
The ``--quiet`` option makes it also **suitable as a cron job**.
or to **download all pictures from your feed**:
To get a list of other helpful flags, run ``instaloader --help``.
::
Usage as library
instaloader --login=your_username :feed-all
Advanced Options
----------------
The following flags can be given to Instaloader to specify how profiles should
be downloaded.
--fast-update Stop when encountering the first already-downloaded post
of a profile.
--profile-pic-only Only download profile pictures. Without this flag, the current
profile picture and all the profile's posts are downloaded.
--skip-videos Skip posts which are videos.
--geotags Also **download geotags** and store Google Maps links in
separate textfiles.
--count COUNT If used with ``#hashtag``, ``:feed-all`` or
``:feed-liked``: Do not attempt to download more than COUNT
posts.
--quiet Do not output any messages except warnings and errors. This
option makes Instaloader **suitable as a cron job**.
--no-sleep Normally, Instaloader waits a few seconds between requests
to the Instagram servers. This flag inhibits this behavior.
--password PASSWORD If used with ``--login``, use parameter as password if no
valid session file is found, instead of asking
interactively.
--sessionfile FILE Specify an alternative place for loading and storing the
session cookies. Without this flag, they are stored in a path
within your temporary directory, encoding your local
username and your instagram profile name.
To get a list of all flags, run ``instaloader --help``.
Usage as Python module
----------------------
You may also use parts of Instaloader as library to do other interesting
things.
For example, to get a list of all followers of a profile as well as
For example, to get a list of all followees of a profile as well as
their follower count, do
.. code:: python
@@ -115,7 +159,14 @@ You could also download your last 20 liked pics with
.. code:: python
instaloader.download_feed_pics(session, max_count=20, fast_update=True,
filter_func=lambda node: not node["likes"]["viewer_has_liked"])
filter_func=lambda node:
not node["likes"]["viewer_has_liked"] if "likes" in node else not node["viewer_has_liked"])
To download the last 20 pictures with hashtag #cat, do
.. code:: python
instaloader.download_hashtag('cat', session=instaloader.get_anonymous_session(), max_count=20)
Each Instagram profile has its own unique ID which stays unmodified even
if a user changes his/her username. To get said ID, given the profile's
+164 -57
View File
@@ -40,6 +40,9 @@ class NonfatalException(InstaloaderException):
class ProfileNotExistsException(NonfatalException):
pass
class ProfileAccessDeniedException(NonfatalException):
pass
class ProfileHasNoPicsException(NonfatalException):
pass
@@ -61,18 +64,17 @@ def _log(*msg, sep='', end='\n', flush=False, quiet=False):
print(*msg, sep=sep, end=end, flush=flush)
def get_json(name: str, session: requests.Session, max_id: int = 0, sleep: bool = True) -> Optional[Dict[str, Any]]:
def get_json(name: str, session: requests.Session,
max_id: Optional[str] = None, sleep: bool = True) -> Optional[Dict[str, Any]]:
"""Return JSON of a profile"""
if max_id == 0:
if not max_id:
resp = session.get('https://www.instagram.com/'+name)
else:
resp = session.get('https://www.instagram.com/'+name, params={'max_id': max_id})
if sleep:
time.sleep(4 * random.random() + 1)
match = re.search('window\\._sharedData = .*<', resp.text)
if match is None:
return None
else:
if match is not None:
return json.loads(match.group(0)[21:-2])
@@ -80,7 +82,7 @@ def get_username_by_id(session: requests.Session, profile_id: int) -> str:
"""To get the current username of a profile, given its unique ID, this function can be used.
session is required to be a logged-in (i.e. non-anonymous) session."""
tempsession = copy_session(session)
tempsession.headers.update({'Content-Type' : 'application/json'})
tempsession.headers.update({'Content-Type' : 'application/x-www-form-urlencoded'})
resp = tempsession.post('https://www.instagram.com/query/', data='q=ig_user(' +
str(profile_id) +')+%7B%0A++username%0A%7D%0A')
if resp.status_code == 200:
@@ -91,8 +93,8 @@ def get_username_by_id(session: requests.Session, profile_id: int) -> str:
"you (id: " + str(profile_id) + ").")
else:
if test_login(session):
raise ConnectionException("Username could not be determined due to connection error" +
" (id: "+ str(profile_id) +").")
raise ProfileAccessDeniedException("Username could not be determined due to error {0} (id: {1})."
.format(str(resp.status_code), str(profile_id)))
raise LoginRequiredException("Login required to determine username (id: " +
str(profile_id) + ").")
@@ -101,7 +103,7 @@ def get_id_by_username(profile: str) -> int:
"""Each Instagram profile has its own unique ID which stays unmodified even if a user changes
his/her username. To get said ID, given the profile's name, you may call this function."""
data = get_json(profile, get_anonymous_session())
if len(data["entry_data"]) == 0 or "ProfilePage" not in data["entry_data"]:
if "ProfilePage" not in data["entry_data"]:
raise ProfileNotExistsException("Profile {0} does not exist.".format(profile))
return int(data['entry_data']['ProfilePage'][0]['user']['id'])
@@ -143,7 +145,7 @@ def get_followees(profile: str, session: requests.Session) -> List[Dict[str, Any
"&ref=relationships%3A%3Afollow_list"]
tmpsession.headers.update(default_http_header())
tmpsession.headers.update({'Referer' : 'https://www.instagram.com/'+profile+'/following/'})
tmpsession.headers.update({'Content-Type' : 'application/json'})
tmpsession.headers.update({'Content-Type' : 'application/x-www-form-urlencoded'})
resp = tmpsession.post('https://www.instagram.com/query/', data=query[0]+"first("+query[1])
if resp.status_code == 200:
data = json.loads(resp.text)
@@ -167,14 +169,18 @@ def get_followees(profile: str, session: requests.Session) -> List[Dict[str, Any
raise LoginRequiredException("Login required to gather followees.")
def download_pic(name: str, url: str, date_epoch: Real, outputlabel: Optional[str] = None, quiet: bool = False) -> bool:
def download_pic(name: str, url: str, date_epoch: Real, outputlabel: Optional[str] = None, quiet: bool = False,
filename_suffix: Optional[str] = None) -> bool:
"""Downloads and saves picture with given url under given directory with given timestamp.
Returns true, if file was actually downloaded, i.e. updated."""
if outputlabel is None:
outputlabel = _epoch_to_string(date_epoch)
urlmatch = re.search('\\.[a-z]*\\?', url)
file_extension = url[-3:] if urlmatch is None else urlmatch.group(0)[1:-1]
filename = name.lower() + '/' + _epoch_to_string(date_epoch) + '.' + file_extension
filename = name.lower() + '/' + _epoch_to_string(date_epoch)
if filename_suffix is not None:
filename += '_' + filename_suffix
filename += '.' + file_extension
if os.path.isfile(filename):
_log(outputlabel + ' exists', end=' ', flush=True, quiet=quiet)
return False
@@ -234,7 +240,7 @@ def save_caption(name: str, date_epoch: Real, caption: str, shorter_output: bool
os.utime(filename, (datetime.datetime.now().timestamp(), date_epoch))
def save_location(name: str, location_json: Dict[str, str], date_epoch: Real) -> None:
def save_location(name: str, location_json: Dict[str, str], date_epoch: Real, quiet: bool = False) -> None:
filename = name.lower() + '/' + _epoch_to_string(date_epoch) + '_location.txt'
location_string = location_json["name"]+"\n" + \
"https://maps.google.com/maps?q={0},{1}&ll={0},{1}\n" \
@@ -243,6 +249,7 @@ def save_location(name: str, location_json: Dict[str, str], date_epoch: Real) ->
with open(filename, 'wb') as text_file:
shutil.copyfileobj(BytesIO(location_string.encode()), text_file)
os.utime(filename, (datetime.datetime.now().timestamp(), date_epoch))
_log('geo', end=' ', flush=True, quiet=quiet)
def download_profilepic(name: str, url: str, quiet: bool = False) -> None:
@@ -284,7 +291,7 @@ def save_session(session: requests.Session, username: str, filename: Optional[st
if filename is None:
filename = get_default_session_filename(username)
dirname = os.path.dirname(filename)
if not os.path.exists(dirname):
if dirname != '' and not os.path.exists(dirname):
os.makedirs(dirname)
os.chmod(dirname, 0o700)
with open(filename, 'wb') as sessionfile:
@@ -423,18 +430,21 @@ def get_feed_json(session: requests.Session, end_cursor: str = None, sleep: bool
"profile_pic_url%2C%0A++username%0A%7D%0A&ref=feed::show"
tmpsession.headers.update(default_http_header())
tmpsession.headers.update({'Referer' : 'https://www.instagram.com/'})
tmpsession.headers.update({'Content-Type' : 'application/json'})
tmpsession.headers.update({'Content-Type' : 'application/x-www-form-urlencoded'})
resp = tmpsession.post('https://www.instagram.com/query/', data=query)
if sleep:
time.sleep(4 * random.random() + 1)
return json.loads(resp.text)
def get_location(node: Dict[str, str], session: requests.Session, sleep: bool = True) -> Dict[str, str]:
pic_json = get_json("p/" + node["code"], session, sleep=sleep)
if pic_json["entry_data"]["PostPage"][0]["media"]["location"] is not None:
def get_location(session: requests.Session, node_code: str, sleep: bool = True) -> Dict[str, str]:
pic_json = get_json("p/" + node_code, session, sleep=sleep)
media = pic_json["entry_data"]["PostPage"][0]["graphql"]["shortcode_media"] \
if "graphql" in pic_json["entry_data"]["PostPage"][0] \
else pic_json["entry_data"]["PostPage"][0]["media"]
if media["location"] is not None:
location_json = get_json("explore/locations/" +
pic_json["entry_data"]["PostPage"][0]["media"]["location"]["id"],
media["location"]["id"],
session, sleep=sleep)
return location_json["entry_data"]["LocationsPage"][0]["location"]
@@ -455,22 +465,51 @@ def download_node(node: Dict[str, Any], session: requests.Session, name: str,
:param quiet: Suppress output
:return: True if something was downloaded, False otherwise, i.e. file was already there
"""
downloaded = download_pic(name, node["display_src"], node["date"], quiet=quiet)
if sleep:
time.sleep(1.75 * random.random() + 0.25)
if "caption" in node:
save_caption(name, node["date"], node["caption"], shorter_output, quiet)
# pylint:disable=too-many-branches,too-many-locals
date = node["date"] if "date" in node else node["taken_at_timestamp"]
if '__typename' in node:
if node['__typename'] == 'GraphSidecar':
sidecar_data = session.get('https://www.instagram.com/p/' + node['code'] + '/', params={'__a': 1}).json()
edge_number = 1
downloaded = True
media = sidecar_data["graphql"]["shortcode_media"] if "graphql" in sidecar_data else sidecar_data["media"]
for edge in media['edge_sidecar_to_children']['edges']:
edge_downloaded = download_pic(name, edge['node']['display_url'],date,
filename_suffix=str(edge_number), quiet=quiet,
outputlabel=(str(edge_number) if edge_number != 1 else None))
downloaded = downloaded and edge_downloaded
edge_number += 1
if sleep:
time.sleep(1.75 * random.random() + 0.25)
elif node['__typename'] in ['GraphImage', 'GraphVideo']:
downloaded = download_pic(name, node["display_url"] if "display_url" in node else node["display_src"],
date, quiet=quiet)
if sleep:
time.sleep(1.75 * random.random() + 0.25)
else:
_log("Warning: Unknown typename discovered:" + node['__typename'])
downloaded = False
else:
# Node is an old image or video.
downloaded = download_pic(name, node["display_src"], date, quiet=quiet)
if sleep:
time.sleep(1.75 * random.random() + 0.25)
if "edge_media_to_caption" in node and node["edge_media_to_caption"]["edges"]:
save_caption(name, date, node["edge_media_to_caption"]["edges"][0]["node"]["text"], shorter_output, quiet)
elif "caption" in node:
save_caption(name, date, node["caption"], shorter_output, quiet)
else:
_log("<no caption>", end=' ', flush=True, quiet=quiet)
node_code = node['shortcode'] if 'shortcode' in node else node['code']
if node["is_video"] and download_videos:
video_data = get_json('p/' + node["code"], session, sleep=sleep)
video_data = get_json('p/' + node_code, session, sleep=sleep)
download_pic(name,
video_data['entry_data']['PostPage'][0]['media']['video_url'],
node["date"], 'mp4', quiet=quiet)
video_data['entry_data']['PostPage'][0]['graphql']['shortcode_media']['video_url'],
date, 'mp4', quiet=quiet)
if geotags:
location = get_location(node, session, sleep)
location = get_location(session, node_code, sleep)
if location:
save_location(name, location, node["date"])
save_location(name, location, date, quiet=quiet)
_log(quiet=quiet)
return downloaded
@@ -484,7 +523,8 @@ def download_feed_pics(session: requests.Session, max_count: int = None, fast_up
Example to download up to the 20 pics the user last liked:
>>> download_feed_pics(load_session('USER'), max_count=20, fast_update=True,
>>> filter_func=lambda node: not node["likes"]["viewer_has_liked"])
>>> filter_func=lambda node:
>>> not node["likes"]["viewer_has_liked"] if "likes" in node else not node["viewer_has_liked"])
:param session: Session belonging to a user, i.e. not an anonymous session
:param max_count: Maximum count of pictures to download
@@ -496,12 +536,20 @@ def download_feed_pics(session: requests.Session, max_count: int = None, fast_up
:param sleep: Sleep between requests to instagram server
:param quiet: Suppress output
"""
# pylint:disable=too-many-locals
data = get_feed_json(session, sleep=sleep)
count = 1
while data["feed"]["media"]["page_info"]["has_next_page"]:
for node in data["feed"]["media"]["nodes"]:
while True:
if "graphql" in data:
is_edge = True
feed = data["graphql"]["user"]["edge_web_feed_timeline"]
else:
is_edge = False
feed = data["feed"]["media"]
for edge_or_node in feed["edges"] if is_edge else feed["nodes"]:
if max_count is not None and count > max_count:
return
node = edge_or_node["node"] if is_edge else edge_or_node
name = node["owner"]["username"]
if filter_func is not None and filter_func(node):
_log("<pic by %s skipped>" % name, flush=True, quiet=quiet)
@@ -513,8 +561,59 @@ def download_feed_pics(session: requests.Session, max_count: int = None, fast_up
sleep=sleep, shorter_output=shorter_output, quiet=quiet)
if fast_update and not downloaded:
return
data = get_feed_json(session, end_cursor=data["feed"]["media"]["page_info"]["end_cursor"],
sleep=sleep)
if not feed["page_info"]["has_next_page"]:
break
data = get_feed_json(session, end_cursor=feed["page_info"]["end_cursor"], sleep=sleep)
def get_hashtag_json(hashtag: str, session: requests.Session,
max_id: Optional[str] = None, sleep: bool = True) -> Optional[Dict[str, Any]]:
"""Return JSON of a #hashtag"""
return get_json(name='explore/tags/{0}/'.format(hashtag), session=session, max_id=max_id, sleep=sleep)
def download_hashtag(hashtag: str, session: requests.Session,
max_count: Optional[int] = None,
filter_func: Optional[Callable[[Dict[str, Dict[str, Any]]], bool]] = None,
fast_update: bool = False, download_videos: bool = True, geotags: bool = False,
shorter_output: bool = False, sleep: bool = True, quiet: bool = False) -> None:
"""Download pictures of one hashtag.
To download the last 30 pictures with hashtag #cat, do
>>> download_hashtag('cat', session=get_anonymous_session(), max_count=30)
:param hashtag: Hashtag to download, without leading '#'
:param session: Session belonging to a user, i.e. not an anonymous session
:param max_count: Maximum count of pictures to download
:param filter_func: function(node), which returns True if given picture should not be downloaded
:param fast_update: If true, abort when first already-downloaded picture is encountered
:param download_videos: True, if videos should be downloaded
:param geotags: Download geotags
:param shorter_output: Shorten log output by not printing captions
:param sleep: Sleep between requests to instagram server
:param quiet: Suppress output
"""
data = get_hashtag_json(hashtag, session, sleep=sleep)
count = 1
while data:
for node in data['entry_data']['TagPage'][0]['tag']['media']['nodes']:
if max_count is not None and count > max_count:
return
_log('[{0:3d}] #{1} '.format(count, hashtag), end='', flush=True, quiet=quiet)
if filter_func is not None and filter_func(node):
_log('<skipped>', quiet=quiet)
continue
count += 1
downloaded = download_node(node, session, '#{0}'.format(hashtag),
download_videos=download_videos, geotags=geotags, sleep=sleep,
shorter_output=shorter_output, quiet=quiet)
if fast_update and not downloaded:
return
if data['entry_data']['TagPage'][0]['tag']['media']['page_info']['has_next_page']:
data = get_hashtag_json(hashtag, session, sleep=sleep,
max_id=data['entry_data']['TagPage'][0]['tag']['media']['page_info']['end_cursor'])
else:
break
def check_id(profile: str, session: requests.Session, json_data: Dict[str, Any], quiet: bool = False) -> str:
@@ -554,9 +653,10 @@ def check_id(profile: str, session: requests.Session, json_data: Dict[str, Any],
raise ProfileNotExistsException("Profile {0} does not exist.".format(profile))
def download(name: str, session: requests.Session, profile_pic_only: bool = False, download_videos: bool = True,
geotags: bool = False,
fast_update: bool = False, shorter_output: bool = False, sleep: bool = True, quiet: bool = False) -> None:
def download(name: str, session: requests.Session,
profile_pic_only: bool = False, download_videos: bool = True, geotags: bool = False,
fast_update: bool = False, shorter_output: bool = False, sleep: bool = True,
quiet: bool = False) -> None:
"""Download one profile"""
# pylint:disable=too-many-branches,too-many-locals
# Get profile main page json
@@ -584,17 +684,13 @@ def download(name: str, session: requests.Session, profile_pic_only: bool = Fals
if data["config"]["viewer"] is not None:
_log("profile %s could also be downloaded anonymously." % name, quiet=quiet)
if ("nodes" not in data["entry_data"]["ProfilePage"][0]["user"]["media"] or
len(data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"]) == 0) \
not data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"]) \
and not profile_pic_only:
raise ProfileHasNoPicsException("Profile %s: no pics found." % name)
# Iterate over pictures and download them
def get_last_id(data):
if len(data["entry_data"]) == 0 or \
len(data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"]) == 0:
return None
else:
data = data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"]
return int(data[len(data) - 1]["id"])
if data["entry_data"] and data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"]:
return data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"][-1]["id"]
totalcount = data["entry_data"]["ProfilePage"][0]["user"]["media"]["count"]
count = 1
while get_last_id(data) is not None:
@@ -626,7 +722,7 @@ def get_logged_in_session(username: str, password: Optional[str] = None, quiet:
def download_profiles(profilelist: List[str], username: Optional[str] = None, password: Optional[str] = None,
sessionfile: Optional[str] = None,
sessionfile: Optional[str] = None, max_count: Optional[int] = None,
profile_pic_only: bool = False, download_videos: bool = True, geotags: bool = False,
fast_update: bool = False,
sleep: bool = True, shorter_output: bool = False, quiet: bool = False) -> None:
@@ -646,26 +742,32 @@ def download_profiles(profilelist: List[str], username: Optional[str] = None, pa
try:
# Generate set of targets
for pentry in profilelist:
if pentry[0] == '@' and username is not None:
if pentry[0] == '#':
_log("Retrieving pictures with hashtag {0}".format(pentry), quiet=quiet)
download_hashtag(hashtag=pentry[1:], session=session, max_count=max_count, fast_update=fast_update,
download_videos=download_videos, geotags=geotags, shorter_output=shorter_output,
sleep=sleep, quiet=quiet)
elif pentry[0] == '@' and username is not None:
_log("Retrieving followees of %s..." % pentry[1:], quiet=quiet)
followees = get_followees(pentry[1:], session)
targets.update([followee['username'] for followee in followees])
elif pentry == ":feed-all" and username is not None:
_log("Retrieving pictures from your feed...", quiet=quiet)
download_feed_pics(session, fast_update=fast_update,
download_feed_pics(session, fast_update=fast_update, max_count=max_count,
download_videos=download_videos, geotags=geotags,
shorter_output=shorter_output, sleep=sleep, quiet=quiet)
elif pentry == ":feed-liked" and username is not None:
_log("Retrieving pictures you liked from your feed...", quiet=quiet)
download_feed_pics(session, fast_update=fast_update,
filter_func=lambda node: not node["likes"]["viewer_has_liked"],
download_feed_pics(session, fast_update=fast_update, max_count=max_count,
filter_func=lambda node:
not node["likes"]["viewer_has_liked"]
if "likes" in node
else not node["viewer_has_liked"],
download_videos=download_videos, geotags=geotags,
shorter_output=shorter_output, sleep=sleep, quiet=quiet)
else:
targets.add(pentry)
if len(targets) == 0:
_log("No profiles to download given.", quiet=quiet)
elif len(targets) > 1:
if len(targets) > 1:
_log("Downloading %i profiles..." % len(targets), quiet=quiet)
# Iterate through targets list and download them
for target in targets:
@@ -685,7 +787,7 @@ def download_profiles(profilelist: List[str], username: Optional[str] = None, pa
print(err, file=sys.stderr)
except KeyboardInterrupt:
print("\nInterrupted by user.", file=sys.stderr)
if len(targets) > 1 and len(failedtargets) > 0:
if len(targets) > 1 and failedtargets:
print("Errors occured (see above) while downloading profiles: %s." %
", ".join(failedtargets), file=sys.stderr)
# Save session if it is useful
@@ -695,8 +797,9 @@ def download_profiles(profilelist: List[str], username: Optional[str] = None, pa
def main():
parser = ArgumentParser(description=__doc__,
epilog="Report issues at https://github.com/Thammus/instaloader/issues.")
parser.add_argument('profile', nargs='*',
help='Name of profile to download; If --login is given: @<profile> to download all followees of '
parser.add_argument('profile', nargs='*', metavar='profile|#hashtag',
help='Name of profile or #hashtag to download. '
'Alternatively, if --login is given: @<profile> to download all followees of '
'<profile>; or the special targets :feed-all or :feed-liked to '
'download pictures from your feed (using '
'--fast-update is recommended).')
@@ -722,6 +825,9 @@ def main():
help='Store geotags when available')
parser.add_argument('-F', '--fast-update', action='store_true',
help='Abort at encounter of first already-downloaded picture')
parser.add_argument('-c', '--count',
help='Do not attempt to download more than COUNT posts. '
'Applies only to #hashtag, :feed-all and :feed-liked.')
parser.add_argument('-S', '--no-sleep', action='store_true',
help='Do not sleep between actual downloads of pictures')
parser.add_argument('-O', '--shorter-output', action='store_true',
@@ -732,8 +838,9 @@ def main():
args = parser.parse_args()
try:
download_profiles(args.profile, args.login, args.password, args.sessionfile,
args.profile_pic_only, not args.skip_videos, args.geotags, args.fast_update,
not args.no_sleep, args.shorter_output, args.quiet)
int(args.count) if args.count is not None else None,
args.profile_pic_only, not args.skip_videos, args.geotags, args.fast_update,
not args.no_sleep, args.shorter_output, args.quiet)
except InstaloaderException as err:
raise SystemExit("Fatal error: %s" % err)
+1 -1
View File
@@ -8,7 +8,7 @@ if sys.version_info < (3, 5):
setup(
name='instaloader',
version='1.2.2',
version='1.3',
py_modules=['instaloader'],
url='https://github.com/Thammus/instaloader',
license='MIT',