Compare commits

..
14 Commits
Author SHA1 Message Date
Alexander Graf 0a039b9c8c Release of version 1.2.2 2017-03-19 14:52:45 +01:00
Alexander Graf b1014bfbfd README: Mention how to install from our Git repo 2017-03-19 14:42:02 +01:00
Alexander Graf d246268630 Retry download anonymously if profile not exists
In case you are blocked by a public profile which you intend to
download, the server responds as if the profile would not exist. Now in
this case, we retry the download without using an anonymous session.
2017-03-19 12:52:07 +01:00
Alexander Graf 23a0e32e8e Clarify --login is required for download followees 2017-03-19 12:51:20 +01:00
Alexander Graf 597e5977b2 Release of version 1.2.1 2017-02-13 10:25:54 +01:00
Alexander Graf 00f6f47fa9 fix get_id_by_username() 2017-02-13 10:20:45 +01:00
Alexander Graf 61711f3b6d Fix Typos in README.rst 2017-02-13 10:03:34 +01:00
Alexander Graf be477e8a88 Fix very minor packaging issues
- State in README.rst and setup.py metainfo that we require Python>=3.5

- Let Travis-CI test against newer versions of Python

- Let instaloader --help show where to report issues
2017-02-13 09:57:03 +01:00
Alexander Graf 02509d3c40 Fix downloading (set max_id only if not zero)
This should fix #17.
2017-02-13 09:50:20 +01:00
Alexander Graf 6e3c13b5d3 Fix typo in README.rst 2017-01-06 10:33:38 +01:00
Alexander Graf 84c2a823c4 fix typing 2016-12-22 16:05:25 +01:00
Alexander Graf f2660f7c02 Require Python >= 3.5 2016-12-22 13:27:08 +01:00
Alexander Graf 73f85b3316 Adjust travis yml 2016-12-22 13:24:50 +01:00
Alexander Graf 86f8b2f018 Annotate all types 2016-12-22 13:20:41 +01:00
4 changed files with 81 additions and 64 deletions
+4 -3
View File
@@ -1,11 +1,12 @@
language: python
python:
- "3.3"
- "3.4"
- "3.5"
- "3.5-dev"
- "3.6"
- "3.6-dev"
- "3.7-dev"
- "nightly"
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 instaloader
- 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
+10 -14
View File
@@ -5,34 +5,30 @@ Instaloader
Installation
------------
instaloader is written in Python, thus ensure having
`Python <https://www.python.org/>`__ (at least version 3.3) installed.
Instaloader is written in Python, thus ensure having
`Python <https://www.python.org/>`__ (at least version 3.5) installed.
If you intend to use this tool under Windows, it is recommended
to install
`win-unicode-console <https://pypi.python.org/pypi/win_unicode_console>`__.
If you have `pip <https://pypi.python.org/pypi/pip>`__ installed, you
may install instaloader using
may install Instaloader using
::
pip3 install instaloader
Alternatively, you may `download instaloader from
github <https://github.com/Thammus/instaloader/releases>`__, unzip it
and invoke bundled ``setup.py`` (requiring
`setuptools <https://pypi.python.org/pypi/setuptools>`__) to install it:
Alternatively, to get the most current version of Instaloader from our
`Git repository <https://github.com/Thammus/instaloader>`__:
::
python3 setup.py install [--user]
pip3 install git+https://github.com/Thammus/instaloader
(pass ``--user`` to install it for your user only instead of globally)
instaloader requires
Instaloader requires
`requests <https://pypi.python.org/pypi/requests>`__, which
will be installed automatically by setup.py, if not already installed.
will be installed automatically, if it is not already installed.
How to automatically download pictures from Instagram
-----------------------------------------------------
@@ -52,7 +48,7 @@ To later update your local copy of that profile, you may run
instaloader --fast-update profile [profile ...]
When ``--fast-update`` is given, instaloder terminates when arriving at
When ``--fast-update`` is given, Instaloader terminates when arriving at
the first already-downloaded picture.
Instaloader can also be used to **download private profiles**. To do so,
@@ -86,7 +82,7 @@ To get a list of other helpful flags, run ``instaloader --help``.
Usage as library
----------------
You may also use parts of instaloader as library to do other interesting
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
+63 -41
View File
@@ -6,6 +6,9 @@ of profiles (even if private), from your feed or from all followees of a given p
import re, json, datetime, shutil, os, time, random, sys, pickle, getpass, tempfile
from argparse import ArgumentParser
from io import BytesIO
from numbers import Real
from typing import List, Optional, Any, Dict, Callable
import requests, requests.utils
# To get version from setup.py for instaloader --version
@@ -58,10 +61,12 @@ def _log(*msg, sep='', end='\n', flush=False, quiet=False):
print(*msg, sep=sep, end=end, flush=flush)
def get_json(name, session, max_id=0, sleep=True):
def get_json(name: str, session: requests.Session, max_id: int = 0, sleep: bool = True) -> Optional[Dict[str, Any]]:
"""Return JSON of a profile"""
resp = session.get('http://www.instagram.com/'+name,
params={'max_id': max_id})
if max_id == 0:
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)
@@ -71,7 +76,7 @@ def get_json(name, session, max_id=0, sleep=True):
return json.loads(match.group(0)[21:-2])
def get_username_by_id(session, profile_id):
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)
@@ -92,20 +97,20 @@ def get_username_by_id(session, profile_id):
str(profile_id) + ").")
def get_id_by_username(profile):
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 len(data["entry_data"]) == 0 or "ProfilePage" not in data["entry_data"]:
raise ProfileNotExistsException("Profile {0} does not exist.".format(profile))
return int(data['entry_data']['ProfilePage'][0]['user']['id'])
def _epoch_to_string(epoch):
def _epoch_to_string(epoch: Real) -> str:
return datetime.datetime.fromtimestamp(epoch).strftime('%Y-%m-%d_%H-%M-%S')
def get_followees(profile, session):
def get_followees(profile: str, session: requests.Session) -> List[Dict[str, Any]]:
"""
Retrieve list of followees of given profile
@@ -162,7 +167,7 @@ def get_followees(profile, session):
raise LoginRequiredException("Login required to gather followees.")
def download_pic(name, url, date_epoch, outputlabel=None, quiet=False):
def download_pic(name: str, url: str, date_epoch: Real, outputlabel: Optional[str] = None, quiet: bool = False) -> 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:
@@ -186,7 +191,7 @@ def download_pic(name, url, date_epoch, outputlabel=None, quiet=False):
raise ConnectionException("File \'" + url + "\' could not be downloaded.")
def save_caption(name, date_epoch, caption, shorter_output=False, quiet=False):
def save_caption(name: str, date_epoch: Real, caption: str, shorter_output: bool = False, quiet: bool = False) -> None:
"""Updates picture caption"""
filename = name.lower() + '/' + _epoch_to_string(date_epoch) + '.txt'
pcaption = caption.replace('\n', ' ').strip()
@@ -229,7 +234,7 @@ def save_caption(name, date_epoch, caption, shorter_output=False, quiet=False):
os.utime(filename, (datetime.datetime.now().timestamp(), date_epoch))
def save_location(name, location_json, date_epoch):
def save_location(name: str, location_json: Dict[str, str], date_epoch: Real) -> 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" \
@@ -240,7 +245,7 @@ def save_location(name, location_json, date_epoch):
os.utime(filename, (datetime.datetime.now().timestamp(), date_epoch))
def download_profilepic(name, url, quiet=False):
def download_profilepic(name: str, url: str, quiet: bool = False) -> None:
"""Downloads and saves profile pic with given url."""
date_object = datetime.datetime.strptime(requests.head(url).headers["Last-Modified"], \
'%a, %d %b %Y %H:%M:%S GMT')
@@ -267,14 +272,14 @@ def download_profilepic(name, url, quiet=False):
raise ConnectionException("File \'" + url + "\' could not be downloaded.")
def get_default_session_filename(username):
def get_default_session_filename(username: str) -> str:
"""Returns default session filename for given username."""
dirname = tempfile.gettempdir() + "/" + ".instaloader-" + getpass.getuser()
filename = dirname + "/" + "session-" + username
return filename
def save_session(session, username, filename=None, quiet=False):
def save_session(session: requests.Session, username: str, filename: Optional[str] = None, quiet: bool = False) -> None:
"""Saves requests.Session object."""
if filename is None:
filename = get_default_session_filename(username)
@@ -288,7 +293,7 @@ def save_session(session, username, filename=None, quiet=False):
_log("Saved session to %s." % filename, quiet=quiet)
def load_session(username, filename=None, quiet=False):
def load_session(username: str, filename: Optional[str] = None, quiet: bool = False) -> requests.Session:
"""Returns loaded requests.Session object, or None if not found."""
if filename is None:
filename = get_default_session_filename(username)
@@ -304,7 +309,7 @@ def load_session(username, filename=None, quiet=False):
pass
def copy_session(session):
def copy_session(session: requests.Session) -> requests.Session:
"""Duplicates a requests.Session."""
new = requests.Session()
new.cookies = \
@@ -313,7 +318,7 @@ def copy_session(session):
return new
def test_login(session):
def test_login(session: requests.Session) -> Optional[str]:
"""Returns the Instagram username to which given requests.Session object belongs, or None."""
if session is None:
return
@@ -324,7 +329,7 @@ def test_login(session):
return data['config']['viewer']['username']
def default_http_header(empty_session_only=False):
def default_http_header(empty_session_only: bool = False) -> Dict[str, str]:
"""Returns default HTTP header we use for requests."""
user_agent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' \
'(KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36'
@@ -347,7 +352,7 @@ def default_http_header(empty_session_only=False):
return header
def get_anonymous_session():
def get_anonymous_session() -> requests.Session:
"""Returns our default anonymous requests.Session object."""
session = requests.Session()
session.cookies.update({'sessionid' : '', 'mid' : '', 'ig_pr' : '1', \
@@ -357,7 +362,7 @@ def get_anonymous_session():
return session
def get_session(user, passwd):
def get_session(user: str, passwd: str) -> requests.Session:
"""Log in to instagram with given username and password and return session object"""
session = requests.Session()
session.cookies.update({'sessionid' : '', 'mid' : '', 'ig_pr' : '1', \
@@ -380,7 +385,7 @@ def get_session(user, passwd):
raise ConnectionException('Login error! Connection error!')
def get_feed_json(session, end_cursor=None, sleep=True):
def get_feed_json(session: requests.Session, end_cursor: str = None, sleep: bool = True) -> Dict[str, Any]:
"""
Get JSON of the user's feed.
@@ -425,7 +430,7 @@ def get_feed_json(session, end_cursor=None, sleep=True):
return json.loads(resp.text)
def get_location(node, session, sleep=True):
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:
location_json = get_json("explore/locations/" +
@@ -434,9 +439,9 @@ def get_location(node, session, sleep=True):
return location_json["entry_data"]["LocationsPage"][0]["location"]
def download_node(node, session, name,
download_videos=True, geotags=False,
sleep=True, shorter_output=False, quiet=False):
def download_node(node: Dict[str, Any], session: requests.Session, name: str,
download_videos: bool = True, geotags: bool = False,
sleep: bool = True, shorter_output: bool = False, quiet: bool = False) -> bool:
"""
Download everything associated with one instagram node, i.e. picture, caption and video.
@@ -444,6 +449,7 @@ def download_node(node, session, name,
:param session: Session
:param name: Name of profile to which this node belongs
:param download_videos: True, if videos should be downloaded
:param geotags: Download geotags
:param sleep: Sleep between requests to instagram server
:param shorter_output: Shorten log output by not printing captions
:param quiet: Suppress output
@@ -469,9 +475,10 @@ def download_node(node, session, name,
return downloaded
def download_feed_pics(session, max_count=None, fast_update=False, filter_func=None,
download_videos=True, geotags=False,
shorter_output=False, sleep=True, quiet=False):
def download_feed_pics(session: requests.Session, max_count: int = None, fast_update: bool = False,
filter_func: Optional[Callable[[Dict[str, Dict[str, Any]]], bool]] = None,
download_videos: bool = True, geotags: bool = False,
shorter_output: bool = False, sleep: bool = True, quiet: bool = False) -> None:
"""
Download pictures from the user's feed.
@@ -484,6 +491,7 @@ def download_feed_pics(session, max_count=None, fast_update=False, filter_func=N
:param fast_update: If true, abort when first already-downloaded picture is encountered
:param filter_func: function(node), which returns True if given picture should not be downloaded
: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
@@ -509,7 +517,7 @@ def download_feed_pics(session, max_count=None, fast_update=False, filter_func=N
sleep=sleep)
def check_id(profile, session, json_data, quiet=False):
def check_id(profile: str, session: requests.Session, json_data: Dict[str, Any], quiet: bool = False) -> str:
"""
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.
@@ -546,8 +554,9 @@ def check_id(profile, session, json_data, quiet=False):
raise ProfileNotExistsException("Profile {0} does not exist.".format(profile))
def download(name, session, profile_pic_only=False, download_videos=True, geotags=False,
fast_update=False, shorter_output=False, sleep=True, quiet=False):
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
@@ -599,7 +608,8 @@ def download(name, session, profile_pic_only=False, download_videos=True, geotag
return
data = get_json(name, session, max_id=get_last_id(data), sleep=sleep)
def get_logged_in_session(username, password=None, quiet=False):
def get_logged_in_session(username: str, password: Optional[str] = None, quiet: bool = False) -> requests.Session:
"""Logs in and returns session, asking user for password if needed"""
if password is not None:
return get_session(username, password)
@@ -614,9 +624,12 @@ def get_logged_in_session(username, password=None, quiet=False):
print(err, file=sys.stderr)
password = None
def download_profiles(profilelist, username=None, password=None, sessionfile=None,
profile_pic_only=False, download_videos=True, geotags=False, fast_update=False,
sleep=True, shorter_output=False, quiet=False):
def download_profiles(profilelist: List[str], username: Optional[str] = None, password: Optional[str] = None,
sessionfile: Optional[str] = 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:
"""Download set of profiles and handle sessions"""
# pylint:disable=too-many-branches,too-many-locals
# Login, if desired
@@ -633,7 +646,7 @@ def download_profiles(profilelist, username=None, password=None, sessionfile=Non
try:
# Generate set of targets
for pentry in profilelist:
if pentry[0] == '@':
if 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])
@@ -657,8 +670,16 @@ def download_profiles(profilelist, username=None, password=None, sessionfile=Non
# Iterate through targets list and download them
for target in targets:
try:
download(target, session, profile_pic_only, download_videos,
geotags, fast_update, shorter_output, sleep, quiet)
try:
download(target, session, profile_pic_only, download_videos,
geotags, fast_update, shorter_output, sleep, quiet)
except ProfileNotExistsException as err:
if username is not None:
_log("\"Profile not exists\" - Trying again anonymously, helps in case you are just blocked")
download(target, get_anonymous_session(), profile_pic_only, download_videos,
geotags, fast_update, shorter_output, sleep, quiet)
else:
raise err
except NonfatalException as err:
failedtargets.append(target)
print(err, file=sys.stderr)
@@ -672,11 +693,12 @@ def download_profiles(profilelist, username=None, password=None, sessionfile=Non
save_session(session, username, sessionfile, quiet=quiet)
def main():
parser = ArgumentParser(description=__doc__)
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; @<profile> to download all followees of '
help='Name of profile to download; 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 if --login is given (using '
'download pictures from your feed (using '
'--fast-update is recommended).')
parser.add_argument('--version', action='version',
version=__version__)
+4 -6
View File
@@ -3,12 +3,12 @@
import sys
from setuptools import setup
if sys.version_info < (3, 3):
sys.exit('Instaloader requires Python >= 3.3.')
if sys.version_info < (3, 5):
sys.exit('Instaloader requires Python >= 3.5.')
setup(
name='instaloader',
version='1.2',
version='1.2.2',
py_modules=['instaloader'],
url='https://github.com/Thammus/instaloader',
license='MIT',
@@ -19,7 +19,7 @@ setup(
'profile.',
long_description=open('README.rst').read(),
install_requires=['requests>=2.4'],
python_requires='>=3.3',
python_requires='>=3.5',
entry_points={'console_scripts': ['instaloader=instaloader:main']},
zip_safe=True,
keywords='instagram downloader',
@@ -29,8 +29,6 @@ setup(
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3 :: Only',