Compare commits

...
28 Commits
Author SHA1 Message Date
Alexander Graf 5204990db9 Release of version 4.0 2018-05-02 17:38:07 +02:00
Alexander Graf 94df221da2 Merge branch 'v4-dev' 2018-05-02 17:29:12 +02:00
Alexander Graf e7decba5ee main: Enumerate profiles separated with ' ' in log 2018-05-02 17:27:46 +02:00
Alexander Graf a3a09e1952 Very minor tweak to a paragraph in basic-usage.rst 2018-05-02 17:26:53 +02:00
Alexander Graf b1e55a41a4 Do not log Post's username in download_saved_posts
This improves speed, as get_saved_posts() returns Posts that do not have
owner_profile info. So, printing the owner_username often required an
additional request.
2018-05-01 20:22:52 +02:00
Alexander Graf b5ad92e236 Trivial improvement to Profile.from_id() 2018-05-01 19:52:39 +02:00
Alexander Graf 084cfb102c Handle ValueError on bad profile id file
Fixes "ValueError: invalid literal for int() with base 10: b''", reported in #95.
2018-05-01 15:14:19 +02:00
Alexander Graf 2722da6ae4 Fix anon username from id (find renamed profile)
maybe related to #95, #104, 67ac8f3397.
2018-05-01 13:11:40 +02:00
Alexander Graf 1469064176 Minor documentation tweaks
- Instaloader as Cronjob sections

- Many new internal links to ease navigation
2018-04-30 09:56:43 +02:00
André Koch-Kramer 440c59ecb9 Release of version 3.3.10 2018-04-20 18:51:55 +02:00
André Koch-Kramer c9198d232f Add 'disable=protected-access' to satisfy pylint 2018-04-20 18:47:36 +02:00
André Koch-Kramer e689dadf00 Output warning when fetching special data
When fetching comments, likes, followees or followers, output a warning
that the used queries are rate limited.
2018-04-20 15:34:43 +02:00
André Koch-Kramer d90c05e0a4 Limit GraphQl queries to 20 per 11 minutes
- Set GRAPHQL_PAGE_LENGTH to 50 what appears to be the new working maximum.
- Limit GQL queries to 20 per 666 seconds.
- Remove logic for tracking queries per query identifier as Instagram
  only allows 20 overall GQL queries per sliding window.

Related to #101
2018-04-20 12:59:41 +02:00
Alexander Graf eecaca7c7b Release of version 3.3.9 2018-04-18 16:25:53 +02:00
Alexander Graf 729aa31d86 Reverse story download order to enable fast-update
Fixes #100.
2018-04-18 16:24:47 +02:00
Alexander Graf 94df60292e Fix bug about 429 handling
It caused Instaloader to fail with a TypeError on 429 Too Many Requests.
2018-04-18 16:24:20 +02:00
Alexander Graf 09f2eb3601 Set GRAPHQL_PAGE_LENGTH to 12
This is the length the browser requests. Might alleviate #94.
2018-04-18 11:41:00 +02:00
Alexander Graf 3e904e8a14 Release of version 3.3.8 2018-04-17 13:30:45 +02:00
Alexander Graf f16b99bec0 Fix GraphQL Queries (again)
Fixes #96.
2018-04-17 13:17:50 +02:00
Alexander Graf d316ddf02a Release of version 3.3.7 2018-04-13 18:17:40 +02:00
Alexander Graf efa0b93fda Fix rhx_gis calculation to fix 403 (fix #94) 2018-04-13 18:05:36 +02:00
André Koch-Kramer ced65ea401 Release of version 3.3.6 2018-04-11 14:32:42 +02:00
André Koch-Kramer 73ec884ea4 Fix anonymous GraphQL queries
Closes #94.
2018-04-11 14:19:24 +02:00
Alexander Graf 74d5e35eb8 get_{comments,likes}: save requests
Post.get_{comments,likes} had a logic to save requests if comments/like
count is low. That logic had a bug that is now fixed.

Related to #94.
2018-04-11 11:24:49 +02:00
Alexander Graf 8612712f44 Release of version 3.3.5 2018-04-10 20:57:53 +02:00
Alexander Graf 67ac8f3397 Enforce being logged in for getting username by id
It seems it is not possible at the moment to get username by userid
anonymously.
2018-04-10 20:52:39 +02:00
Alexander Graf 5fe2a70374 Fix downloading of hashtags 2018-04-10 20:34:07 +02:00
Alexander Graf 8bd9c4449c Theme-related Documentation Enhancements
- Consistent background color in all types of codes
- Improve dd (list item) padding
- Link to Index in global TOC in navbar
- Briefly describe Instaloader in `index.html` title
- Remove page contents (navbartoc) in index, genindex and installation
- Link to new pypi.org rather than old pypi.python.org
2018-04-09 16:07:08 +02:00
11 changed files with 92 additions and 33 deletions
+6 -1
View File
@@ -3,6 +3,8 @@
Documentation of Instaloader module, a powerful and intuitive
Python library to download Instagram media and metadata.
.. _python-module-instaloader:
Python Module :mod:`instaloader`
--------------------------------
@@ -72,7 +74,10 @@ metadata of a Profile. :class:`Profile` instances can be created with:
profile = Profile.from_username(L.context, USERNAME)
- :meth:`Profile_from_userid`
given its User ID (currently requires to be logged in).
given its User ID. This allows to easily lookup a Profile's username given
its ID::
Profile.from_id(L.context, USERID).username
- :meth:`Profile.get_followees`
Profiles that are followed by given user.
+37 -1
View File
@@ -5,6 +5,8 @@
and private profiles, hashtags, stories, feeds, saved media, and
their metadata, comments and captions.
.. _download-pictures-from-instagram:
Download Pictures from Instagram
---------------------------------
@@ -35,7 +37,9 @@ To later **update your local copy** of that profiles, you may run
instaloader --fast-update profile [profile ...]
If :option:`--fast-update` is given, Instaloader stops when arriving at the
first already-downloaded picture. When updating profiles, Instaloader
first already-downloaded picture.
When updating profiles, Instaloader
automatically **detects profile name changes** and renames the target directory
accordingly.
@@ -90,6 +94,9 @@ downloads the pictures and videos and their captions. You can specify
**download geotags** of each post and save them as
Google Maps link,
For a reference of all supported command line options, see
:ref:`command-line-options`.
.. _filename-specification:
Filename Specification
@@ -237,3 +244,32 @@ files. Say, you now also want to export the number of comments the Posts had
when they were downloaded::
instaloader --post-metadata-txt="{likes} likes, {comments} comments." <target>/*.json.xz
.. _instaloader-as-cronjob:
Instaloader as Cronjob
^^^^^^^^^^^^^^^^^^^^^^
Instaloader is suitable for running as a cronjob to periodically update your
personal Instagram archive. The :option:`--quiet` option disables user
interactions and logging of non-error messages. To non-interactively use
Instaloader logged-in, create a session file, e.g. in your home directory::
instaloader --login=your_username --sessionfile=~/.instaloader-session
Then use the same parameters in your cronjob to load the session and download
the given targets::
instaloader --login=your_username --sessionfile=~/.instaloader-session --quiet <target> [...]
Without :option:`--sessionfile` option, Instaloader saves the session file in
a path within your temporary directory.
Programming Instaloader
^^^^^^^^^^^^^^^^^^^^^^^
If your task cannot be done with the command line interface of Instaloader,
consider taking a look at the :ref:`python-module-instaloader`.
Instaloader exposes its internally used methods and structures, making it a
powerful and intuitive Python API for Instagram, allowing to further customize
obtaining media and metadata.
+11 -3
View File
@@ -1,3 +1,5 @@
.. _command-line-options:
Command Line Options
====================
@@ -12,7 +14,9 @@ posts marked as saved).
Here we explain the additional options that can be given to Instaloader to
customize its behavior. To get a list of all flags, their abbreviations and
their descriptions, you may also run ``instaloader --help``.
their descriptions, you may also run ``instaloader --help``. For an
introduction on how to use Instaloader, see
:ref:`download-pictures-from-instagram`.
What to Download
^^^^^^^^^^^^^^^^
@@ -142,6 +146,10 @@ Instaloader to login.
Password for your Instagram account. Without this option, you'll be prompted
for your password interactively if there is not yet a valid session file.
.. warning:: Using :option:`--password` option is discouraged for security
reasons. Enter your password interactively when asked, or use the
sessionfile feature (:option:`--sessionfile` to customize path).
How to Download
^^^^^^^^^^^^^^^
@@ -177,5 +185,5 @@ Miscellaneous Options
.. option:: --quiet, -q
Disable user interaction, i.e. do not print messages (except errors) and fail
if login credentials are needed but not given. This makes Instaloader
**suitable as a cron job**.
if login credentials are needed but not given.
This is handy for running :ref:`instaloader-as-cronjob`.
+4
View File
@@ -21,6 +21,8 @@ With `Python <https://www.python.org/>`__ installed, do::
$ instaloader profile [profile ...]
See :ref:`install` for more options on how to install Instaloader.
**Instaloader**
- downloads **public and private profiles, hashtags, user stories,
@@ -44,6 +46,8 @@ With `Python <https://www.python.org/>`__ installed, do::
profile | "#hashtag" |
:stories | :feed | :saved
See :ref:`download-pictures-from-instagram` for a detailed introduction on how
to use Instaloader to download pictures from Instagram.
Instaloader Documentation
-------------------------
+3 -3
View File
@@ -33,7 +33,7 @@ Install Instaloader
- On **Windows 10**, you may download the standalone executable from the
`current release page <https://github.com/instaloader/instaloader/releases/latest>`__.
To get the most current version of Instaloader from our
`Git repository <https://github.com/instaloader/instaloader>`__::
- To get the most current version of Instaloader from our
`Git repository <https://github.com/instaloader/instaloader>`__::
pip3 install --upgrade git+https://github.com/instaloader/instaloader
pip3 install --upgrade git+https://github.com/instaloader/instaloader
+1 -1
View File
@@ -1,7 +1,7 @@
"""Download pictures (or videos) along with their captions and other metadata from Instagram."""
__version__ = '4.0rc1'
__version__ = '4.0'
try:
+1 -1
View File
@@ -137,7 +137,7 @@ def _main(instaloader: Instaloader, targetlist: List[str],
else:
profiles.add(target)
if len(profiles) > 1:
instaloader.context.log("Downloading {} profiles: {}".format(len(profiles), ','.join(profiles)))
instaloader.context.log("Downloading {} profiles: {}".format(len(profiles), ' '.join(profiles)))
# Iterate through profiles list and download them
for target in profiles:
with instaloader.context.error_catcher(target):
+3 -8
View File
@@ -529,11 +529,10 @@ class Instaloader:
for post in Profile.from_username(self.context, self.context.username).get_saved_posts():
if max_count is not None and count > max_count:
break
name = post.owner_username
if post_filter is not None and not post_filter(post):
self.context.log("<pic by {} skipped".format(name), flush=True)
self.context.log("<{} skipped>".format(post), flush=True)
continue
self.context.log("[{:>3}] {} ".format(count, name), end=str(), flush=True)
self.context.log("[{:>3}] ".format(count), end=str(), flush=True)
count += 1
with self.context.error_catcher('Download saved posts'):
downloaded = self.download_post(post, target=':saved')
@@ -629,10 +628,6 @@ class Instaloader:
else:
self.context.log("Trying to find profile {0} using its unique ID {1}.".format(profile_name,
profile_id))
if not self.context.is_logged_in:
self.context.error("Profile {} changed its name. "
"If you use --login=USERNAME, I can find out the new name.")
raise LoginRequiredException("--login=USERNAME required to obtain profile name from its ID number")
profile_from_id = Profile.from_id(self.context, profile_id)
newname = profile_from_id.username
self.context.log("Profile {0} has changed its name to {1}.".format(profile_name, newname))
@@ -647,7 +642,7 @@ class Instaloader:
'{0}/{1}_id'.format(self.dirname_pattern.format(), newname.lower()))
return profile_from_id
return profile
except FileNotFoundError:
except (FileNotFoundError, ValueError):
pass
if profile_exists:
os.makedirs(self.dirname_pattern.format(profile=profile_name.lower(),
+12
View File
@@ -56,6 +56,7 @@ class InstaloaderContext:
self.quiet = quiet
self.max_connection_attempts = max_connection_attempts
self._graphql_page_length = 50
self._root_rhx_gis = None
# error log, filled with error() and printed at the end of Instaloader.main()
self.error_log = []
@@ -376,3 +377,14 @@ class InstaloaderContext:
except KeyboardInterrupt:
self.error("[skipped by user]", repeat_at_end=False)
raise ConnectionException(error_string) from err
@property
def root_rhx_gis(self) -> Optional[str]:
"""rhx_gis string returned in the / query."""
if self.is_logged_in:
# At the moment, rhx_gis seems to be required for anonymous requests only. By returning None when logged
# in, we can save the root_rhx_gis lookup query.
return None
if not self._root_rhx_gis:
self._root_rhx_gis = self.get_json('', {})['rhx_gis']
return self._root_rhx_gis
+10 -15
View File
@@ -139,20 +139,17 @@ class Post:
@property
def owner_profile(self) -> 'Profile':
""":class:`Profile` instance of the Post's owner."""
if not self._owner_profile:
if 'username' in self._node['owner']:
owner_struct = self._node['owner']
else:
# Sometimes, the 'owner' structure does not contain the username, only the user's ID. In that case,
# this call triggers downloading of the complete Post metadata struct, where the owner username
# is contained. This is better than to get the username by user ID, since it is possible anonymously
# and gives us other information that is more likely to be usable.
# is contained.
# Note that we cannot use Profile.from_id() here since that would lead us into a recursion.
owner_struct = self._full_metadata['owner']
if 'username' in owner_struct:
self._owner_profile = Profile(self._context, owner_struct)
else:
# Fallback, if we still did not get the owner username
self._owner_profile = Profile.from_id(self._context, owner_struct['id'])
self._owner_profile = Profile(self._context, owner_struct)
return self._owner_profile
@property
@@ -379,17 +376,16 @@ class Profile:
@classmethod
def from_id(cls, context: InstaloaderContext, profile_id: int):
"""If logged in, create a Profile instance from a given userid. If possible, use :meth:`Profile.from_username`
or constructor directly rather than this method, since does many requests.
"""Create a Profile instance from a given userid. If possible, use :meth:`Profile.from_username`
or constructor directly rather than this method, since it does many requests.
:param context: :attr:`Instaloader.context`
:param profile_id: userid
:raises: :class:`ProfileNotExistsException`, :class:`LoginRequiredException`, :class:`ProfileHasNoPicsException`
:raises: :class:`ProfileNotExistsException`, :class:`ProfileHasNoPicsException`
"""
if not context.is_logged_in:
raise LoginRequiredException("--login=USERNAME required to obtain profile metadata from its ID number.")
data = context.graphql_query("472f257a40c653c64c666ce877d59d2b",
{'id': str(profile_id), 'first': 1})['data']['user']
{'id': str(profile_id), 'first': 1},
rhx_gis=context.root_rhx_gis)['data']['user']
if data:
data = data["edge_owner_to_timeline_media"]
else:
@@ -400,8 +396,7 @@ class Profile:
raise ProfileHasNoPicsException("Profile with ID {0}: no pics found.".format(str(profile_id)))
else:
raise LoginRequiredException("Login required to determine username (ID: " + str(profile_id) + ").")
username = Post.from_mediaid(context, int(data['edges'][0]["node"]["id"])).owner_username
return cls(context, {'username': username.lower(), 'id': profile_id})
return Post(context, data['edges'][0]['node']).owner_profile
def _asdict(self):
json_node = self._node.copy()
+4
View File
@@ -54,6 +54,10 @@ class TestInstaloaderAnonymously(unittest.TestCase):
self.assertEqual(PUBLIC_PROFILE_ID,
instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).userid)
def test_get_username_by_id(self):
self.assertEqual(PUBLIC_PROFILE.lower(),
instaloader.Profile.from_id(self.L.context, PUBLIC_PROFILE_ID).username)
def test_post_from_mediaid(self):
for post in instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).get_posts():
post2 = instaloader.Post.from_mediaid(self.L.context, post.mediaid)