Compare commits

...
223 Commits
Author SHA1 Message Date
Alexander Graf e9648bc551 Release of Version 4.5.4 2020-11-28 16:31:05 +01:00
Alexander Graf 6d90c47d9a Troubleshooting: Mention overriding RateController 2020-11-28 16:29:18 +01:00
Alexander Graf 1c577716dd Fix HTTP error 500 when accessing :stories in certain cases
Based on a comment in #795:
https://github.com/instaloader/instaloader/issues/795#issuecomment-726634493

Closes #795.
2020-11-28 16:15:51 +01:00
Alexander Graf 6824d7b75c Update year in LICENSE file 2020-11-28 16:13:10 +01:00
Alexander Graf 6ad03d3575 Link Insights -> Contributors in AUTHORS.md 2020-11-28 16:11:45 +01:00
Alexander Graf 03ac067e71 Add Contributor Covenant Code of Conduct 2020-11-28 16:08:31 +01:00
Rafael Ristovski e70faf506d Update graphql query hash used for stories (#872)
This updated graph query hash returns all fields for `tappable_objects`. These fields were missing/partial when using the old query hash.
This allows to get the full json metadata of a story.
2020-11-19 11:02:32 +01:00
Alexander Graf cc2986cff3 Fix Hashtag.get_all_posts() missing one post in certain cases
When a hashtag query returned only the 9 top posts, get_all_posts() yielded only 8 instead of all 9 posts.

Closes #853.
2020-11-14 20:09:24 +01:00
Alexander Graf c7372e36cc Require being logged in for Post.get_likes()
Closes #860.
2020-11-14 18:57:49 +01:00
Alexander Graf 74f7970b50 Fix a KeyError when fetching post metadata fails
Fixes #824.
2020-11-13 11:20:31 +01:00
André Koch-Kramer f4ecfea64c Add Android to docs installation page 2020-09-28 21:14:38 +02:00
Alexander Graf 0586a1fec3 Update docs/troubleshooting.rst 2020-09-19 17:07:57 +02:00
Ingo Marquardt c53625028d Avoid additional http request to get post's owner ID. (#809) 2020-09-18 11:23:06 +02:00
André Koch-Kramer 4ad4584b0d Release of Version 4.5.3 2020-09-03 20:23:57 +02:00
886530d661 Fix wrong ProfileNotExistsException in some edge cases (#785)
Fixes wrong "Profile does not exist" error.

Co-authored-by: André Koch-Kramer <koch-kramer@web.de>
Co-authored-by: Alexander Graf <17130992+aandergr@users.noreply.github.com>
2020-09-03 20:20:02 +02:00
Alexander Graf 574cadbb82 Adjust bug report template 2020-08-31 20:35:13 +02:00
Alexander Graf 6d3c73eec3 Adjust RateController to current observations 2020-08-31 20:29:21 +02:00
Alexander Graf 438669b954 Release of Version 4.5.2 2020-08-23 18:55:22 +02:00
Alexander Graf a7a05c200c Adjust RateController to current observations
Fixes #774.
2020-08-23 18:51:39 +02:00
Alexander Graf f53bdf9ef8 Do not call get_sidecar_nodes() if not needed
Do not call Post.get_sidecar_nodes() in download_post() if both --no-pictures
and --no-videos are set.
2020-08-23 18:48:21 +02:00
Alexander Graf 13612c606e Use more reliable query for own profile for :saved
Adds a method Profile.own_profile() which uses the test_login() query for
obtaining the own profile. This method is used for accesssing saved posts
(Profile.get_saved_posts() on own profile).

Also removes the now-unneeded self._obtain_metadata() call in
Profile.get_saved_posts(), and changes the NodeIterator such that data is never
None and the count property is more likely available.

Fixes #563.
2020-08-23 18:46:34 +02:00
Alexander Graf 3a6ba6485e Release of Version 4.5.1 2020-08-20 11:08:08 +02:00
Alexander Graf 177a299ced Improve 429 error message
This text_for_429 error message is expected to be less confusing for new users.
Further improve the output formatting of _dump_query_timestamps().
2020-08-20 10:38:06 +02:00
Alexander Graf 4a1fe035af First Release Candidate for Version 4.5.1rc1 2020-08-17 18:29:37 +02:00
Alexander Graf db86f81be7 Fix support for Python 3.5 prior Python 3.5.4 (#777) 2020-08-17 18:25:22 +02:00
Alexander Graf 020d412342 Fix some video download edge cases (#776)
Fixes unintended sidecar video skipping if --no-pictures is set.

Fixes video download if video thumbnail access fails with 410 error.
2020-08-17 18:23:24 +02:00
lapourgagner 6f57197afb Fixed issue #769 by using os.replace instead os.rename (#775) 2020-08-17 18:17:23 +02:00
Alexander Graf 57393f619d doc: Don't force links to be opened in new tab 2020-08-09 08:08:17 +02:00
Alexander Graf d5dd8cec00 Fix Downloads/Month Badge
Change to pepy.tech as source for this badge.
2020-08-06 09:43:22 +02:00
Alexander Graf 3652e009d9 Release of Version 4.5 2020-08-02 10:32:44 +02:00
Alexander Graf ecde92d05e Minor fix of Hashtag.get_all_posts() docstring 2020-08-02 10:23:09 +02:00
Alexander Graf a8d3851956 Nicer syntax for resumable_iteration usage 2020-08-02 10:22:45 +02:00
Alexander Graf c71873231d Minor Documentation Fixes (mostly spelling&grammar) 2020-07-31 22:12:27 +02:00
Alexander Graf 3823b1e593 Documentation Template: Use ARIA roles
This fixes the full text functionality of the search.
2020-07-31 11:41:22 +02:00
Alexander Graf 6c779e8d4b First Beta Release for Version 4.5 2020-07-30 17:17:55 +02:00
Alexander Graf c24015a93d Merge branch 'master' into upcoming/v4.5 2020-07-30 17:16:00 +02:00
Alexander Graf 2c56e6994a Enhance Documentation Theme
Improvements on our bootstrap 4 theme for sphinx.
2020-07-30 17:02:43 +02:00
Alexander Graf 6791e9a2f8 Textual Improvements on Documentation
- Fix spelling errors / typos,

- Remove deprecated command line options from doc.
2020-07-30 16:41:45 +02:00
Alexander Graf 64652f3994 Feature Request template: PR -> pull request 2020-07-30 13:20:02 +02:00
Alexander Graf 4f5b223b60 Minor docs fixes 2020-07-26 19:00:01 +02:00
Alexander Graf 2a3711687f Update deps used in CI (esp. MyPy and PyLint) 2020-07-26 18:54:23 +02:00
Alexander Graf 612842f255 Minor nodeiterator-related docs improvements 2020-07-26 18:05:46 +02:00
Alexander Graf fe9cd653e7 docs: Improvements to branded syntax highlighting 2020-07-26 18:01:33 +02:00
Alexander Graf 82e7b19d6d docs: Split as-module into smaller documents 2020-07-26 18:00:37 +02:00
Alexander Graf 5d7c36399f Windows: Sanitize Path of Resume Files 2020-07-24 18:35:10 +02:00
Alexander Graf f0e66ef465 Windows EXE: Fix CommandNotFoundException
Fixes CommandNotFoundException that occured when started from within a
directory whose name contains a space.
2020-07-24 17:19:48 +02:00
Alexander Graf f71ea562af First Alpha Release for Version 4.5 2020-07-21 17:37:45 +02:00
Alexander Graf bc40b82f94 Resume a previously-aborted post download loop (#732)
With this change, Instaloader is capable of resuming a previously-aborted download loop. To do so, it creates a JSON file within the target directory when interrupted, that contains all the necessary information to later resume that operation.

Resuming an interrupted download is supported for most, but not all targets. It is supported for:

- Regular profile posts,
- IGTV posts
- Saved posts,
- Tagged posts,
- Explore posts.
2020-07-21 17:28:55 +02:00
Alexander Graf c817d1901a Merge branch 'master' into upcoming/v4.5 2020-07-18 15:32:05 +02:00
Alexander Graf e232c82d5e Release of Version 4.4.5 2020-07-18 15:24:31 +02:00
Alexander Graf 601dd0b106 First Release Candidate for Version 4.4.5 2020-07-15 10:26:22 +02:00
Alexander Graf 4c20b573e2 Fix KeyError on Posts with very few comments 2020-07-15 10:17:20 +02:00
Cadence EmberandAlexander Graf db63e5c7e5 Bypass IP-based redirects to /accounts/login (#727)
* Bypass IP-based redirects to /accounts/login

There are two main changes made:

For users, we request /{username}/feed/ instead of /{username}/. For some
reason, this completely bypasses the login redirect. This page doesn't
work in browser while blocked, but fortunately all the data we need is
just present in the HTML page.

For posts, we change from using the /p/ page to using the graphql
endpoint for the same data, which is still subject to graphql rate
limits, but is not subject to login redirects. The data is identical
between the two pages, apart from the object keys being sorted
differently and rhx_gis being missing on graphql.

Yes, this now unblocks access from VPNs, Tor, cloud servers, etc.

* Apply requested patch to fix comments

* Remove rhx_gis from Post and Profile

Co-authored-by: Alexander Graf <17130992+aandergr@users.noreply.github.com>
2020-07-15 09:46:39 +02:00
Alexander Graf fbe05a1add More flexible Rate Controller (#716)
Move InstaloaderContext's rate controlling logic into a class
RateController with fine-grained methods to enable easily changing
Instaloader's behavior regarding rate limits.
2020-07-11 13:54:31 +02:00
Alexander Graf e21d34124d Add codesnippet for almost chronological order
Such as for downloading hashtag feeds, as discussed in #666 and contributed by
@e2tovar.

Also change comment color to grey in codesnippets in documentation.
2020-06-21 19:38:16 +02:00
Lars Lindqvist fa62025ea3 Deprecate commit-mode (#697)
Rather than checking the json file to make sure posts have been
successfully downloaded, data is stored in a temporary file which
is renamed when downloading has finished, as suggested in #257.
2020-06-20 16:23:31 +02:00
michalens 66b3ec6928 Add explanation on downloading single post (#694) 2020-06-19 19:39:08 +02:00
Alexander Graf f7d82495bd Consistently use native '\' path separator on Windows
Use os.path.join() for joining paths rather than +'/'+.

os.path.join() uses '/' on Unix-ish and '\' on Windows. On Windows, this makes
the following improvements:

- Visual consistency with / and \ not being mixed-up in one path (os.path.join()
  has already been used in many places).

- Allow using the \\?\ bypass method, e.g. with
    --dirname-pattern=\\?\D:\{target}
  The \\?\ bypass method fails when '/' is used as path separator. With the \\?\
  bypass method, we can create directories whose name would otherwise be
  considered invalid, such as directories starting with "aux.".
2020-06-19 19:33:46 +02:00
Alexander Graf b0a430051e Restrict GH Action lint workflow on Python 3.8
Now that Travis-CI status reporting works again, there is no need to run the
tests twice.
2020-06-18 08:33:25 +02:00
Alexander Graf 8b56b90e72 Less intrusive PR template 2020-06-18 08:32:46 +02:00
Alexander Graf 0c93e63615 Update troubleshooting.rst, Minor fixes to contributing.rst 2020-06-16 21:44:22 +02:00
Alexander Graf 65025e61d9 Update issue/PR templates, contributing.rst, CI
- Keep documentation in sync with master branch

- Run PyLint and MyPy on Github Actions

- Update Issue templates, enforce usage of issue templates, add links to
  resources in Instaloader Documentation

- Update contributing.rst
2020-06-16 20:26:58 +02:00
Alexander Graf 11baf493db Release of Version 4.4.4 2020-06-11 20:10:52 +02:00
Alexander Graf 2345d90e0e Fix docs building on Windows 2020-06-09 13:03:48 +02:00
Alexander Graf 71b09aed0f Warn if --login parameter is not a username
Closes #624.
2020-06-09 11:44:06 +02:00
Alexander Graf fd6d8542c9 Add a unit test for an empty private profile 2020-06-09 11:44:06 +02:00
Alexander Graf df6fbbee76 Third Release Candidate for Version 4.4.4 2020-06-08 18:51:46 +02:00
Alexander Graf 1661bf7722 Remove unused import 2020-06-08 18:44:43 +02:00
Alexander Graf 8f9723cf62 Fix "Use --login ..." warning at startup 2020-06-08 18:40:13 +02:00
Alexander Graf e1567a0342 Second Release Candidate for Version 4.4.4 2020-06-08 18:22:06 +02:00
Alexander Graf 610b6242a0 Get higher image quality if logged-in
Addresses a suggestion in #630.
2020-06-08 17:53:39 +02:00
Alexander Graf d90f90988a Fix ValueError when executing from other drive on Windows
Fixes #671.
2020-06-08 17:01:42 +02:00
Alexander Graf d3c747a306 First Release Candidate for Version 4.4.4 2020-06-06 10:52:42 +02:00
Alexander Graf 225353dc21 Download highest-resolution images if logged-in (#662)
If logged-in, Post.url and Post.get_sidecar_nodes() now use the iPhone API
endpoint to obtain the highest-resolution image version. Resolves #630.

A notice "Warning: Use --login to download higher-quality versions of pictures"
is issued if the user tries to download posts without being logged-in.

Further, the message "Errors occurred:", which is displayed when the
InstaloaderContext is closed, has been changed to "Errors or warnings
occurred:".
2020-06-06 10:51:03 +02:00
Alexander Graf ed499cb49c Let --sessionfile default to a more persistent path (#659)
The presence of a sessionfile avoids the usage of the failure-prone login
mechanism.  This commit addresses a suggestion in #615 to store the sessionfile
in a persistent path rather than a path within a temporary directory if no
--sessionfile parameter is given.

The default path is now:

  $XDG_CONFIG_HOME/instaloader/session-USERNAME or
    ~/.config/instaloader/session-USERNAME on Unix,

  %LOCALAPPDATA%\Instaloader\session-USERNAME on Windows.

If no file exists in the new path, Instaloader tries loading from the path
where the sessionfile was stored before this commit, hence it automatically
migrates to the new sessionfile path.
2020-06-06 10:49:41 +02:00
Alexander Graf 1c13d5a30b Windows EXE: Start Powershell instead of cmd
... if started by file explorer.
2020-06-06 10:37:20 +02:00
Alexander Graf 2fd1422619 Windows EXE: Use psutil for startup terminal detection 2020-06-05 20:12:23 +00:00
Alexander Graf c818165db4 Update 615_import_firefox_session.py
This fixes finding the cookies.sqlite file on MacOS as per https://github.com/instaloader/instaloader/issues/92#issuecomment-638361344.
2020-06-03 20:07:52 +02:00
Alexander Graf 26a78463f1 doc: Discourage using --commit-mode
--commit-mode has many bugs, especially #257, #483.  For now we discourage using
it until it is reimplemented.
2020-06-03 18:03:27 +02:00
Alexander Graf 689dd3613b doc: Minor style-related changes to code examples 2020-06-03 18:03:01 +02:00
Alexander Graf 27b7c98be2 Update doc section Troubleshooting -> Login Error
New Firefox cookie import script, based on a comment in #615:
https://github.com/instaloader/instaloader/issues/615#issuecomment-630127064
2020-06-03 17:14:08 +02:00
Alexander Graf 72d86533f2 Downgrade 'old' stale action to more stable v1 2020-05-26 16:03:52 +02:00
Alexander Graf 7bb08a220a Update issue stale action 2020-05-24 11:25:37 +02:00
Alexander Graf 03aabc52bb Release of Version 4.4.3 2020-05-22 18:32:05 +02:00
fireattack 9d4df0d5fb Only substitute characters from auto generated fields (#631)
Only substitute characters from auto generated fields in _PostPathFormatter

This fixes issues of path problem on Windows such as #304, #529, etc.

Also made the following changes:

1. Changes substitution of colon to full-width colon (U+FF1A) instead of MODIFIER LETTER COLON (U+A789) to be consistent with others (we use full-width backslash, pipe, etc. already).
2. Also replaces `\r` on Windows with space, just like `\n`.
2020-05-22 18:30:55 +02:00
Alexander Graf bf2f4f16ed Release of Version 4.4.2 2020-05-19 09:27:03 +02:00
InfinitayandAlexander Graf 1a3712d112 Workaround for Instagram's encrypted login. (#623)
* Workaround for Instagram's encrypted login.

Credit: https://github.com/pgrimaud/
See: https://github.com/pgrimaud/instagram-user-feed/commit/96ad4cf54d1ad331b337f325c73e664999a6d066

Closes #615.

Co-authored-by: Alexander Graf <17130992+aandergr@users.noreply.github.com>
2020-05-19 09:25:08 +02:00
Alexander Graf 65462b01ec Release of Version 4.4.1 2020-05-14 16:22:42 +02:00
Alexander Graf 9857c5fa24 Regex target matching: Fix hashtag regex
Unicode characters are allowed in hashtags, so instead of just
[A-Za-z0-9], we now match all \w.  Fixes #614.
2020-05-14 16:17:07 +02:00
Alexander Graf 83f013f6ca Release of Version 4.4 2020-05-11 20:25:43 +02:00
Alexander Graf e7437310ea Add versionadded doc annotation for is_business_account and business_category_name 2020-05-11 20:24:04 +02:00
yolo dcba25a40e [Issue #601] Add is_business_account and business_category_name properties to Profile structure 2020-05-06 08:45:31 +02:00
Alexander Graf 5e4b5fd877 First Beta Release for Version 4.4 2020-04-21 10:22:03 +02:00
Lars Lindqvist 657e76e2d9 Simplify username regex
Without passing re.ASCII, \w matches non-ascii letter, such as ö, π
and я, which to my knowledge isn't allowed in usernames. Now it's
explicit which character ranges are expected in each target.
2020-04-21 10:15:24 +02:00
Lars Lindqvist 78023b3c98 Regex target matching 2020-04-21 10:15:24 +02:00
Alexander Graf ab67ca30e5 Merge branch 'master' into v4.4-dev 2020-04-21 09:53:01 +02:00
Alexander Graf 0e13c73b62 Include top posts in download_hashtag() 2020-04-19 12:21:27 +02:00
Alexander Graf b98e9941ae Omit media when saving Hashtag JSON 2020-04-19 11:30:19 +02:00
Alexander Graf c9e4f68597 Download Hashtag profilepic and save metadata JSON 2020-04-19 11:16:39 +02:00
Alexander Graf 1338cf0fcd Fix 'occured' typo in error messages
Mentioned in #573 - thanks!
2020-04-18 11:16:55 +02:00
Alexander Graf 83a0f52c60 Hashtag class 2020-04-17 18:49:38 +02:00
Alexander Graf 5c5c633bee Catch Exceptions raised during filter evaluation
Closes #192.
2020-04-10 14:39:40 +02:00
Alexander Graf e268f9e625 Release of Version 4.3.2 2020-04-10 12:08:24 +02:00
Alexander Graf 793b7e82ba Limit feed node processing to Post nodes
Fixes #478.
2020-04-10 12:05:15 +02:00
Alexander Graf 7329ee10d8 Catch KeyError: 'authenticated' when Logging In
Fixes #472.
2020-04-10 12:05:15 +02:00
Alexander Graf 57d2ce9538 Alleviate #449 by catching bad metadata response
Instead of ungracefully failing with a TypeError, the Post is now skipped with
an error message containing the invalid JSON.
2020-04-10 11:57:36 +02:00
Alexander Graf dc458a3f3c Limit feed node processing to Post nodes
Fixes #478.
2020-04-10 11:13:07 +02:00
Alexander Graf 08a359408c Improve Log Output of Skipped Posts 2020-04-10 11:12:09 +02:00
Alexander Graf a794089c4f Deduplicate Post Download Loop 2020-04-07 10:06:53 +02:00
Alexander Graf c56657855d Release of Version 4.3.1 2020-04-04 10:54:04 +02:00
Alexander Graf 8c7561c5d7 Fix Downloading of Videos within a Sidecar Post
Fixes #558.
2020-04-04 10:51:55 +02:00
Alexander Graf c76385bde2 Unit Test for Profile.get_similar_accounts() 2020-03-25 19:06:05 +01:00
Alexander Graf b5bf8a0560 Implement Profile.get_similar_accounts()
Fixes #489.
2020-03-25 19:00:03 +01:00
Alexander Graf 2790342a09 Catch KeyError: 'authenticated' when Logging In
Fixes #472.
2020-03-25 18:17:02 +01:00
Alexander Graf e725ca2640 Post properties is_sponsored and sponsor_users
This allows to access the Paid Partnership With metadata of a Post.

Closes #295.
2020-03-25 17:13:46 +01:00
Alexander Graf 9c2e7c70ac Fix number output in download_igtv() 2020-03-16 13:38:13 +01:00
Alexander Graf ffbf5b517c Release of Version 4.3 2020-03-16 09:13:25 +01:00
Alexander Graf 0843e10187 First Release Candidate for Version 4.3 2020-03-15 14:28:16 +01:00
Alexander Graf 78a93c945e Revert "Revert "Remove nonworking saythanks.io link""
saythanks.io does not work again and the link is now removed.
This reverts commit b2acbcf0e0.
2020-03-14 17:56:44 +01:00
Alexander Graf bb3e309195 First Beta Release for Version 4.3 2020-03-14 17:21:07 +01:00
Alexander Graf 6cc2ed54ea Download IGTV Videos of a Profile (--igtv) 2020-03-14 16:56:51 +01:00
Alexander Graf 41cf260fdd Fix post filter for downloading tagged posts 2020-03-14 16:55:37 +01:00
Alexander Graf f910705c51 docs: Fix formatting of request_timeout param 2020-03-07 11:14:07 +01:00
Alexander Graf aa3f19dcbe Second Alpha Release for Version 4.3 2020-03-01 17:44:17 +01:00
Alexander Graf fe258f1abb Also Download Highlight Cover Pictures
Instaloader.download_highlights(), as well as the Instaloader CLI with
--highlight option, now also downloads the cover picture of each highlight.

This closes #323, where it was suggested.
2020-03-01 17:41:58 +01:00
Alexander Graf 534497bea5 fix misplacement of --request-timeout in docs 2020-02-28 09:49:29 +01:00
André Koch-Kramer 819426efdb First Alpha Release for Version 4.3 2020-02-17 18:53:33 +01:00
Alexander Graf 0aac91680f docs: versionadded/changed directives for 4.3 2020-02-17 18:47:45 +01:00
André Koch-Kramer dc289024c8 GitHub Actions workflow for Windows EXE 2020-02-17 18:31:36 +01:00
Alexander Graf b2acbcf0e0 Revert "Remove nonworking saythanks.io link"
saythanks.io works again. This reverts commit
f966ea5bb9.
2020-02-04 09:23:02 +01:00
Alexander Graf de585becdb Rate-control non-graphql queries
Non-GraphQL queries seem to be rate limited too, and this commit adds
a safe-seeming rate-controlling for this kind of queries.

This fixes #464.
2020-02-03 17:19:49 +01:00
Alexander Graf f966ea5bb9 Remove nonworking saythanks.io link
saythanks.io stopped working, BlitzKraft/saythanks.io#60.
2020-01-27 21:31:47 +01:00
André Koch-Kramer 302145f54f Print profile suggestions when profile not found
Relates to #496.
2020-01-27 13:06:42 +01:00
André Koch-Kramer 8764947d10 Raise QueryReturnedNotFoundException if expected structure is missing
- QueryReturnedNotFoundException now inherits ConnectionException
  --> Retry on 404 errors + missing "window._sharedData"

Relates to #146 and #496.
2020-01-27 13:06:42 +01:00
André Koch-Kramer d3403bc686 Implementation of TopSearchResults class
To be able to search Instagram for profiles, hashtags and locations.
2020-01-27 13:06:42 +01:00
Alexander Graf b7bae19374 Update mypy and pylint versions for CI 2020-01-27 09:27:47 +01:00
Alexander Graf 4e87a8cf63 Fix wrong request_timeout in anonymous_copy() context
Fixes a tiny mistake introduced with #422.
2020-01-26 22:07:07 +01:00
Alexander Graf fd13d66367 Deploy documentation on tag only
Before, the documentation was pushed to GitHub Pages whenever a new commit has been pushed to master. I.e., when a new feature was added, the documentation advertised the feature although hasn’t been released yet.

Now, we only deploy the newly-build the documentation when a new tag is pushed, and ensure the the versionadded directives are added before releasing.
2020-01-26 18:54:11 +01:00
corpone2 63eb9e71ca support passing timeout to requests (#422)
Adds a request_timeout parameter to Instaloader instantiation.

Closes #330.
2020-01-13 21:59:00 +01:00
André Koch-Kramer d8ba30b91b Speed up accessing property comments of Post
This has been asked for in #436.
2020-01-13 19:56:50 +01:00
André Koch-Kramer ec0e017b10 Release of version 4.2.10 2019-10-28 12:48:27 +01:00
51bfa01c2a Solving KeyError: 'edge_sidecar_to_children' (#411)
Closes #380.
Closes #394.

Co-Authored-By: Dāvis <davispuh@users.noreply.github.com>
Co-Authored-By: Alexander Graf <mail@agraf.me>
Co-Authored-By: Thammus <koch-kramer@web.de>
2019-10-21 19:47:14 +02:00
Alexander Graf e8d4054f84 Create Stale Issue Action 2019-10-19 16:20:25 +02:00
Leonard Bogdonoff cf293ce312 Change Example Hashtag in Codesnippets 2019-10-13 19:47:44 +02:00
Alexander Graf 3226c43a98 Fix Intersphinx Mapping for Requests to fix CI 2019-10-13 19:45:14 +02:00
corpone 20819d2597 handle alternate rate limit exceeded behavior of redirecting to login 2019-08-22 19:53:52 -07:00
Alexander Graf 4e8236b6e9 Improve contributing.rst and issue templates 2019-08-20 22:05:49 +02:00
André Koch-Kramer b542936c09 Release of version 4.2.9 2019-08-18 18:28:35 +02:00
Ahmad af4dbc00d7 Added likes_count to PostCommentAnswer tuple (#379) 2019-08-18 18:18:35 +02:00
Alexander Graf 0aa1ec7c76 Getting locations now requires --login
Closes #376.
2019-08-18 10:48:46 +02:00
Alexander Graf 75a729781d 194_top_x_of_user example: Fix sort order 2019-08-10 16:25:22 +02:00
Alexander Graf a88316224b Remove ',' after URL in "Checkpoint required" message
Closes #364.
2019-08-10 16:19:35 +02:00
Alexander Graf 87dc82d1b1 Travis: Skip test with Python 3.8 beta 2019-07-27 22:04:56 +02:00
Alexander Graf 6a3b05762e Fix missing directory error in some rare cases 2019-07-27 21:08:29 +02:00
Alexander Graf 6d5bb1e181 Release of Version 4.2.8 2019-05-27 18:26:10 +02:00
Alexander Graf 819057eb6f travis: use Pipfile for dependency pinning 2019-05-27 18:15:45 +02:00
Alexander Graf 5235cc0f6a Travis: test against Python 3.8 2019-05-27 18:08:17 +02:00
Alexander Graf fb06b4b7d6 Fix iPhone query rate control
Resolves #306.
2019-05-27 18:05:51 +02:00
Alexander Graf 1a382cd61c Fixes to unittests
- Fix type issue with unittest.SkipTest

- Fix query timestamp preservation
2019-05-17 21:58:51 +02:00
André Koch-Kramer 0345da71ec Release of version 4.2.7 2019-05-17 21:44:11 +02:00
André Koch-Kramer 749ff60639 Do not require rhx_gix in metadata
Closes #297.
2019-05-17 21:38:39 +02:00
Alexander Graf 3528e8d19b Release of Version 4.2.6 2019-05-08 22:18:29 +02:00
Alexander Graf 5fce6946ea AUR deploy: Do not deploy prereleases 2019-05-08 22:10:15 +02:00
Alexander Graf 692cbc000d Use stronger pylint configuration 2019-05-08 21:49:06 +02:00
Alexander Graf 102f27c4ef doc: Add versionadded directives to new Post properties 2019-05-08 21:31:40 +02:00
Alexander Graf 33d13c4b32 Fix import issue with Python 3.5
Closes #293.
2019-05-08 21:24:00 +02:00
Alexander Graf 408cf22c18 Fix target directory with tagged and highlights
Fixes #292.
2019-05-08 21:21:16 +02:00
André Koch-Kramer 93882d93e0 Release of version 4.2.5 2019-05-07 18:04:19 +02:00
Alexander Graf 8ef7b3242c Add trendy Badges to README.rst and index.rst 2019-05-07 18:01:13 +02:00
André Koch-Kramer 406f33896a First Release Candidate for Version 4.2.5 2019-05-07 17:08:32 +02:00
André Koch-Kramer b9be1eaaf4 Automation of arch package deploy to AUR 2019-05-07 16:40:43 +02:00
Alexander Graf 6b24b54591 Post properties video_view_count and video_duration
Closes #239.
2019-05-07 15:47:52 +02:00
Alexander Graf 67d3b4fca1 Configure pylint via .pylintrc 2019-05-07 15:11:36 +02:00
Alexander Graf 4c72077976 Integrate mypy static type checker into CI 2019-05-07 13:01:16 +02:00
André Koch-Kramer 4f9d64a284 Replace non filename compatible chars in fields
Values that got inserted in dirname and filename pattern get cleared
from characters that will not work in filenames.
This is also helpful for #262.
2019-05-06 17:42:59 +02:00
André Koch-Kramer 0c9a667da7 Replace slash and backslash with similar unicode 2019-05-06 17:41:20 +02:00
Friendly ccf55dbeef New Format Specifier: {pcaption}
I saw the abbreviated caption output from save_caption and realized it was perfect for --filename-pattern.  Even if this pull request is not accepted, please do not abandon the idea within.  The ability to put captions in file names is *very* useful.
2019-04-16 19:49:31 -04:00
Dāvis 87d877e650 There can be comments without any answers 2019-04-14 00:40:46 +03:00
André Koch-Kramer adb9f3a346 Release of version 4.2.4 2019-04-01 21:46:09 +02:00
André Koch-Kramer 2aa26ea140 Disable pylint check (no-else-raise)
Freeze pylint version to 2.3.1.
2019-03-31 16:15:51 +02:00
André Koch-Kramer 0fd06fe6bc Remove needlessly complicated code 2019-03-31 15:55:25 +02:00
André Koch-Kramer 15d9cd8949 Adjust structure to handle new threaded comments
This is needed because of a structure change by Instagram.
Fixes #272.
2019-03-31 11:29:07 +02:00
Alexander Graf 9421ad5fda Release of version 4.2.3 2019-02-16 22:25:43 +01:00
Alexander Graf 1e89ebe038 Adjust GraphQL rate control 2019-02-16 19:26:58 +01:00
André Koch-Kramer 3f4c12b54b Release of version 4.2.2 2019-02-10 21:13:29 +01:00
Alexander Graf dcc57f11c4 Fix falsely-raised PostChangedException 2019-02-10 20:55:07 +01:00
Alexander Graf a9e79e90f9 Fix retry logic for download of files
This fixes #235. The retry logic has been broken with commit
4bc0a94e12 in version 4.2.1.
2019-02-10 20:29:04 +01:00
André Koch-Kramer a0b7804fd2 Properly handle redirected posts
Resolves #225.
2019-02-10 18:24:33 +01:00
André Koch-Kramer 9e04ef3436 New Profile.from_id() that works without posts
The old method needed the target profile to have at least one post.
The new method works anonymously even for private profiles.
Closes #249.
2019-02-10 17:46:18 +01:00
Alexander Graf 34416298a0 profiles: First load profilepic, then save json
This way, the iphone_struct information is included in saved profile pictures.
2019-01-24 22:40:36 +01:00
Alexander Graf e39ffecea4 Minor doc changes
- Add saythanks.io link
- Set lang="en" attribute in html tag
2019-01-24 22:39:58 +01:00
Alexander Graf 15fe3d5f2d doc: versionadded directive to new {write,get}_raw 2019-01-17 18:39:54 +01:00
Alexander Graf c73b10843f Update year in LICENSE file 2019-01-17 18:12:32 +01:00
Alexander Graf b158c57b9e Unittest for HD profilepic and fix ResourceWarning 2019-01-17 18:10:14 +01:00
Alexander Graf 57892f191a Release of version 4.2.1 2019-01-17 17:47:45 +01:00
Alexander Graf 26e74bad6a Download HD profile picture if logged in
Unfortunately, it is now required to be logged in to access the HD version of
profile pictures. When attempting to download profile pictures without --login,
a warning message is printed once, and the lower-quality versions are obtained.

For backwards compatibility, already-downloaded profile pictures are
overwritten if the now-obtainable version is assumed to be of better quality
than the existing one (determined by file size vs Content-Length).

The iPhone endpoint is accessed with code exhumed from
c355338010. Also, this reverts "Profile: don't
access removed iphone info endpoint" 08327c4117.

This fixes #209.
2019-01-17 17:39:29 +01:00
Alexander Graf 27a239f07f doc theme: Improve consistency of code highlight 2019-01-17 11:39:18 +01:00
Alexander Graf bb25f03c7d doc: "Troubleshooting" section + minor changes 2019-01-17 11:00:04 +01:00
Lars Lindqvist c54342bb98 Refactor get_filename
Use string formatting instead of `str(0) + str(index)` and don't assume file extension is three characters.
2019-01-08 19:38:46 +01:00
Alexander Graf d2bebcd00c doc: Fix +args.txt description
- Fix wrong `--option value`, which must be `--option=value`
- Fix typos

Fixes #221.
2019-01-03 17:05:43 +01:00
Alexander Graf 4bc0a94e12 Name profilepic by hash if Last-Modified missing
This fixes #188.

Also, this commit prevents double-requesting the profile pic URL to obtain the
Last-Modified header.
2019-01-03 16:49:21 +01:00
Alexander Graf 06845b53fc Fixes and enhancements to documentation theme 2019-01-01 14:23:08 +01:00
Alexander Graf e9cec0b261 docs/conf.py: remove unneeded import 2018-12-31 19:35:55 +01:00
Alexander Graf ac4cd9f595 New doc design: Sphinx Bootstrap 4 Theme
Now we use an own, mobile-first, responsive sphinx theme based on Bootstrap 4.
It makes navigation on the very long "Python module instaloader" page a lot
easier. Also, it looks better.

This solves all the problems we had with sphinx-bootstrap-theme.
2018-12-31 19:30:19 +01:00
Alexander Graf 2e7835850a Version 4.2 2018-12-23 17:07:03 +01:00
Alexander Graf 89b56bdfa8 Login: Handle suspicious login and JSON error
Motivated by #92.
2018-12-21 22:39:07 +01:00
Alexander Graf 1387d396dd Second Release Candidate for Version 4.2 2018-12-21 22:01:28 +01:00
André Koch-Kramer 4ee867c61b Implement two-factor authentication
Closes #200.
2018-12-21 21:58:12 +01:00
Alexander Graf edba6959d9 First Release Candidate for Version 4.2 2018-12-21 17:54:02 +01:00
Alexander Graf 08327c4117 Profile: don't access removed iphone info endpoint
Don't access the endpoint that was used to access the HD version of the profile
picture. This addresses #209.
2018-12-21 17:06:32 +01:00
Alexander Graf 4ab0fea82a login: print that 2FA is not supported if required
2FA support is requested in issue #200, and also mentioned in #92.
2018-12-21 16:54:56 +01:00
Alexander Graf a735b02bac doc: Add 'versionadded' directives to new features 2018-12-21 14:43:33 +01:00
Stijn Peeters be5d02ef3b Location search (#212)
* Add %location search option

Search for posts for a given location ID using %[location id] as the query

* Document %location search

* Make pylint happy

* Use correct paths for location results

* Fix —help output

Add description of location argument and fix output error for short help.

* Add unit tests for location download

* Add extra unit test for locations
2018-12-17 21:35:31 +01:00
Alexander Graf 1ab9e44104 Update sphinx_autodoc_typehints to version 1.5.2 2018-12-13 16:23:14 +01:00
André Koch-Kramer 6212d22e26 Merge branch 'master' of https://github.com/sushilicious/instaloader into v4.2-dev 2018-12-13 15:53:06 +01:00
André Koch-Kramer aa859b9f54 Ignore pylint's cyclic-import and unnecessary-pass 2018-12-13 15:29:46 +01:00
sushilicious ffda2265bd Added _committed to __init__ to make pylint shut up 2018-12-09 02:12:45 -05:00
sushilicious e753406a6c Just in case, check whether picture exists for commit
This is in addition to the other commit logic.
2018-12-09 02:12:45 -05:00
sushilicious 30555b576c Added commit mode
The commit mode ensures pictures are not corrupted when Instaloader is
unexpectedly interrupted. In the case that the last picture is corrupted
because of an interruption, Instaloader will redownload the picture.
Since the metadata is the last object saved to disk, we can consider a
post as "committed" if its json metadata file exists and is not
malformed. Instaloader should download any posts which are not
committed. Downside is commit mode requires metadata to be saved.
2018-12-09 02:12:45 -05:00
Alexander Graf ece0d11915 Release of version 4.1.1 2018-10-07 15:17:51 +02:00
Alexander Graf 41e734e1b8 Fix fast_update in case of no download
If Instaloader was called with --fast-update and encountered a post where
nothing has been downloaded because of flags that disable download of certain
types of media, the download was aborted. This behavior is now fixed.

Closes #183.
2018-10-07 14:59:16 +02:00
Alexander Graf b7ea0d5953 download_profiles: Check errors only if required
Fixes #178, where with `--profile-pic-only` an error message is printed
(without any further implications) when downloading profile pictures of private
profiles without being logged in.
2018-09-23 10:09:08 +02:00
Alexander Graf 325e89c4b2 Print how many requests were made on 429 2018-09-06 09:17:38 +02:00
62 changed files with 4283 additions and 919 deletions
+6 -3
View File
@@ -1,6 +1,7 @@
---
name: Bug report
about: Create a report to help us improve
name: 🐛 Bug report
about: Create a report to help us improve 🤕
labels: bug
---
@@ -12,7 +13,7 @@ Steps to reproduce the behavior:
(e.g. Instaloader command line)
**Expected behavior**
A clear and concise description of what you expected to happen (if not obvious).
A clear and concise description of what you expected to happen (even if it seems obvious).
**Error messages and tracebacks**
If applicable, add error messages and tracebacks to help explain your problem.
@@ -22,3 +23,5 @@ If applicable, add error messages and tracebacks to help explain your problem.
**Additional context**
Add any other context about the problem here.
<!-- please also see https://instaloader.github.io/contributing.html for how to report a bug -->
+11
View File
@@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Contributing to Instaloader
url: https://instaloader.github.io/contributing.html
about: Guidelines for how to report an issue.
- name: Instaloader Troubleshooting
url: https://instaloader.github.io/troubleshooting.html
about: Many common problems and their workarounds are described here.
- name: Instaloader Documentation
url: https://instaloader.github.io/
about: Many questions might be already answered here.
+9 -2
View File
@@ -1,6 +1,7 @@
---
name: Feature request
about: Suggest an idea for this project
name: 🚀 Feature Suggestion
about: Suggest an idea for this project 💡
labels: "feature suggestion"
---
@@ -13,5 +14,11 @@ A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**If the feature request is accepted, would you be willing to submit a pull request?**
Yes / No
<!-- Help can be provided if you need assistance submitting a PR -->
**Additional context**
Add any other context about the feature request here.
<!-- please also see https://instaloader.github.io/contributing.html for how to suggest a feature -->
+12
View File
@@ -0,0 +1,12 @@
---
name: ❓ Question
about: If you have a question about how to use Instaloader that's not well covered by our documentation
labels: question
---
Your question here...
<!-- Please see our documentation: https://instaloader.github.io/ -->
<!-- Basic questions regarding Instaloader's usage might be more appropriate to ask on Stack Overflow -->
+17
View File
@@ -0,0 +1,17 @@
<!--
Please describe:
- A motivation for this change, e.g.
- Fixes # .
- More general: What problem does the pull request solve?
- The changes proposed in this pull request
- The completeness of this change
- Is it just a proof of concept?
- Is the documentation updated (if appropriate)?
- Do you consider it ready to be merged or is it a draft?
- Can we help you at some point?
-->
+30
View File
@@ -0,0 +1,30 @@
name: PyLint and MyPy
on: [push, pull_request]
jobs:
lint:
name: PyLint and MyPy
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8]
steps:
- name: Checkout Instaloader Repository
uses: actions/checkout@v2
with:
fetch-depth: 0 # needed for building docs
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install Dependencies
run: |
python -m pip install pipenv==2020.6.2
pipenv sync --dev
- name: PyLint
run: pipenv run pylint instaloader
- name: MyPy
run: pipenv run mypy -m instaloader
- name: Build Documentation
run: pipenv run make -C docs html SPHINXOPTS="-W -n"
+34
View File
@@ -0,0 +1,34 @@
name: Mark stale issues and pull requests
on:
schedule:
- cron: "10 14 * * *"
jobs:
stale:
name: Mark stale issues and pull requests
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: 'question'
stale-issue-message: 'There has been no activity on this question for an extended period of time. This issue will be closed after further 14 days of inactivity.'
stale-issue-label: 'stale'
exempt-issue-labels: 'leave open'
days-before-stale: 15
days-before-close: -1
remove-stale-when-updated: false
- uses: actions/stale@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: 'There has been no activity on this issue for an extended period of time. This issue will be closed after further 14 days of inactivity.'
stale-pr-message: 'There has been no activity on this Pull Request for an extended period of time. This Pull Request will be closed after further 14 days of inactivity.'
stale-issue-label: 'stale'
stale-pr-label: 'stale'
exempt-issue-label: 'leave open'
exempt-pr-label: 'leave open'
days-before-stale: 90
days-before-close: 14
+48
View File
@@ -0,0 +1,48 @@
name: Windows EXE build
on: [push, pull_request]
jobs:
build:
name: Windows EXE build
runs-on: windows-latest
steps:
- name: Checkout Instaloader repository
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v1
with:
python-version: 3.8
architecture: x64
- name: Get the tagged version
id: get_version
env:
GITHUB_REF: ${{ github.ref }}
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/}
shell: bash
- name: Run custom python script for EXE creation
env:
VERSION_TAG: v${{ steps.get_version.outputs.VERSION }}
run: python deploy/windows/create_exe.py
- name: Create draft release
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ steps.get_version.outputs.VERSION }}
release_name: Version ${{ steps.get_version.outputs.VERSION }}
draft: true
prerelease: contains(steps.get_version.outputs.VERSION, 'a') || contains(steps.get_version.outputs.VERSION, 'b') || contains(steps.get_version.outputs.VERSION, 'c')
- name: Upload EXE as release asset
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./instaloader-v${{ steps.get_version.outputs.VERSION }}-windows-standalone.zip
asset_name: instaloader-v${{ steps.get_version.outputs.VERSION }}-windows-standalone.zip
asset_content_type: application/zip
+1
View File
@@ -1 +1,2 @@
__pycache__/
.mypy_cache/
+584
View File
@@ -0,0 +1,584 @@
[MASTER]
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-whitelist=
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use.
jobs=0
# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Specify a configuration file.
#rcfile=
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
confidence=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=invalid-name,
missing-docstring,
too-many-lines,
print-statement,
parameter-unpacking,
unpacking-in-except,
old-raise-syntax,
backtick,
long-suffix,
old-ne-operator,
old-octal-literal,
import-star-module-level,
non-ascii-bytes-literal,
raw-checker-failed,
bad-inline-option,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
too-many-instance-attributes,
too-many-public-methods,
too-many-branches,
too-many-arguments,
too-many-locals,
too-many-statements,
no-else-return,
no-else-raise,
unnecessary-pass,
wildcard-import,
unused-wildcard-import,
apply-builtin,
basestring-builtin,
buffer-builtin,
cmp-builtin,
coerce-builtin,
execfile-builtin,
file-builtin,
long-builtin,
raw_input-builtin,
reduce-builtin,
standarderror-builtin,
unicode-builtin,
xrange-builtin,
coerce-method,
delslice-method,
getslice-method,
setslice-method,
no-absolute-import,
old-division,
dict-iter-method,
dict-view-method,
next-method-called,
metaclass-assignment,
indexing-exception,
raising-string,
reload-builtin,
oct-method,
hex-method,
nonzero-method,
cmp-method,
input-builtin,
round-builtin,
intern-builtin,
unichr-builtin,
map-builtin-not-iterating,
zip-builtin-not-iterating,
range-builtin-not-iterating,
filter-builtin-not-iterating,
using-cmp-argument,
eq-without-hash,
div-method,
idiv-method,
rdiv-method,
exception-message-attribute,
invalid-str-codec,
sys-max-int,
bad-python3-import,
deprecated-string-function,
deprecated-str-translate-call,
deprecated-itertools-function,
deprecated-types-field,
next-method-defined,
dict-items-not-iterating,
dict-keys-not-iterating,
dict-values-not-iterating,
deprecated-operator-function,
deprecated-urllib-function,
xreadlines-attribute,
deprecated-sys-function,
exception-escape,
comprehension-escape
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member
[REPORTS]
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
#msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored. Default to name
# with leading underscore.
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
[STRING]
# This flag controls whether the implicit-str-concat-in-sequence should
# generate a warning on implicit string concatenation in sequences defined over
# several lines.
check-str-concat-over-line-jumps=no
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package..
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[SIMILARITIES]
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
# Minimum lines number of a similarity.
min-similarity-lines=4
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
[LOGGING]
# Format style used to check logging format string. `old` means using %
# formatting, while `new` is for `{}` formatting.
logging-format-style=old
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=120
# Maximum number of lines in a module.
max-module-lines=1000
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,
dict-separator
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style.
#argument-rgx=
# Naming style matching correct attribute names.
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style.
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style.
#class-attribute-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style.
#class-rgx=
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style.
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style.
#function-rgx=
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style.
#inlinevar-rgx=
# Naming style matching correct method names.
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style.
#method-rgx=
# Naming style matching correct module names.
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style.
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Naming style matching correct variable names.
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style.
#variable-rgx=
[IMPORTS]
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=optparse,tkinter.tix
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled).
ext-import-graph=
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled).
import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled).
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
[DESIGN]
# Maximum number of arguments for function / method.
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in an if statement.
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=12
# Maximum number of locals for function / method body.
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body.
max-returns=6
# Maximum number of statements in function / method body.
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=cls
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "BaseException, Exception".
overgeneral-exceptions=BaseException,
Exception
+12 -5
View File
@@ -1,15 +1,17 @@
dist: xenial
language: python
python:
- "3.5"
- "3.5-dev"
- "3.6"
- "3.6-dev"
- "3.7"
install:
- pip install pylint requests
- pip install -r docs/requirements.txt
- pipenv sync --dev
script:
- python3 -m pylint -r n -d bad-whitespace,missing-docstring,too-many-arguments,locally-disabled,line-too-long,too-many-public-methods,too-many-lines,too-many-instance-attributes,too-many-locals,too-many-branches,too-many-statements,inconsistent-return-statements,invalid-name,wildcard-import,unused-wildcard-import,no-else-return instaloader
- pylint instaloader
- mypy -m instaloader
- make -C docs html SPHINXOPTS="-W -n"
addons:
ssh_known_hosts: aur.archlinux.org
deploy:
- provider: pypi
user: aandergr
@@ -27,3 +29,8 @@ deploy:
on:
branch: master
python: 3.6
- provider: script
script: deploy/arch/deploy.sh $TRAVIS_TAG
on:
tags: true
python: 3.6
+1
View File
@@ -6,3 +6,4 @@ Instaloader is written by
- Alexander Graf (@aandergr)
- André Koch-Kramer (@Thammus)
- Lars Lindqvist (@e5150)
- ... and many more, see https://github.com/instaloader/instaloader/graphs/contributors
+135
View File
@@ -0,0 +1,135 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement by opening an
issue or contacting one or more of the project maintainers.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
at [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2016-2018 Alexander Graf and André Koch-Kramer.
Copyright (c) 2016-2020 Alexander Graf and André Koch-Kramer.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+16
View File
@@ -0,0 +1,16 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
pylint = "*"
mypy = "*"
sphinx = "~=2.3"
pyinstaller = "*"
pefile = "*"
pywin32-ctypes = "*"
psutil = "*"
[packages]
requests = "*"
Generated
+439
View File
@@ -0,0 +1,439 @@
{
"_meta": {
"hash": {
"sha256": "58caf1be034bed9aa7ebb8c78e75ffa7d7abe820d014b6398c444a5db566b306"
},
"pipfile-spec": 6,
"requires": {},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"certifi": {
"hashes": [
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
],
"version": "==2020.6.20"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.10"
},
"requests": {
"hashes": [
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
],
"index": "pypi",
"version": "==2.24.0"
},
"urllib3": {
"hashes": [
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
],
"version": "==1.25.10"
}
},
"develop": {
"alabaster": {
"hashes": [
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
],
"version": "==0.7.12"
},
"altgraph": {
"hashes": [
"sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa",
"sha256:c623e5f3408ca61d4016f23a681b9adb100802ca3e3da5e718915a9e4052cebe"
],
"version": "==0.17"
},
"astroid": {
"hashes": [
"sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703",
"sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"
],
"version": "==2.4.2"
},
"babel": {
"hashes": [
"sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38",
"sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"
],
"version": "==2.8.0"
},
"certifi": {
"hashes": [
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
],
"version": "==2020.6.20"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"docutils": {
"hashes": [
"sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af",
"sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"
],
"version": "==0.16"
},
"future": {
"hashes": [
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
],
"version": "==0.18.2"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.10"
},
"imagesize": {
"hashes": [
"sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1",
"sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"
],
"version": "==1.2.0"
},
"isort": {
"hashes": [
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
],
"version": "==4.3.21"
},
"jinja2": {
"hashes": [
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
],
"version": "==2.11.2"
},
"lazy-object-proxy": {
"hashes": [
"sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d",
"sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449",
"sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08",
"sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a",
"sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50",
"sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd",
"sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239",
"sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb",
"sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea",
"sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e",
"sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156",
"sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142",
"sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442",
"sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62",
"sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db",
"sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531",
"sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383",
"sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a",
"sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357",
"sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4",
"sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"
],
"version": "==1.4.3"
},
"markupsafe": {
"hashes": [
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
],
"version": "==1.1.1"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"version": "==0.6.1"
},
"mypy": {
"hashes": [
"sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c",
"sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86",
"sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b",
"sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd",
"sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc",
"sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea",
"sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e",
"sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308",
"sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406",
"sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d",
"sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707",
"sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d",
"sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c",
"sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a"
],
"index": "pypi",
"version": "==0.782"
},
"mypy-extensions": {
"hashes": [
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
],
"version": "==0.4.3"
},
"packaging": {
"hashes": [
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
"sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
],
"version": "==20.4"
},
"pefile": {
"hashes": [
"sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"
],
"index": "pypi",
"version": "==2019.4.18"
},
"psutil": {
"hashes": [
"sha256:0ee3c36428f160d2d8fce3c583a0353e848abb7de9732c50cf3356dd49ad63f8",
"sha256:10512b46c95b02842c225f58fa00385c08fa00c68bac7da2d9a58ebe2c517498",
"sha256:4080869ed93cce662905b029a1770fe89c98787e543fa7347f075ade761b19d6",
"sha256:5e9d0f26d4194479a13d5f4b3798260c20cecf9ac9a461e718eb59ea520a360c",
"sha256:66c18ca7680a31bf16ee22b1d21b6397869dda8059dbdb57d9f27efa6615f195",
"sha256:68d36986ded5dac7c2dcd42f2682af1db80d4bce3faa126a6145c1637e1b559f",
"sha256:90990af1c3c67195c44c9a889184f84f5b2320dce3ee3acbd054e3ba0b4a7beb",
"sha256:a5b120bb3c0c71dfe27551f9da2f3209a8257a178ed6c628a819037a8df487f1",
"sha256:d8a82162f23c53b8525cf5f14a355f5d1eea86fa8edde27287dd3a98399e4fdf",
"sha256:f2018461733b23f308c298653c8903d32aaad7873d25e1d228765e91ae42c3f2",
"sha256:ff1977ba1a5f71f89166d5145c3da1cea89a0fdb044075a12c720ee9123ec818"
],
"index": "pypi",
"version": "==5.7.2"
},
"pygments": {
"hashes": [
"sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44",
"sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"
],
"version": "==2.6.1"
},
"pyinstaller": {
"hashes": [
"sha256:3730fa80d088f8bb7084d32480eb87cbb4ddb64123363763cf8f2a1378c1c4b7"
],
"index": "pypi",
"version": "==3.6"
},
"pylint": {
"hashes": [
"sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc",
"sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c"
],
"index": "pypi",
"version": "==2.5.3"
},
"pyparsing": {
"hashes": [
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
"version": "==2.4.7"
},
"pytz": {
"hashes": [
"sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
"sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"
],
"version": "==2020.1"
},
"pywin32-ctypes": {
"hashes": [
"sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942",
"sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"
],
"index": "pypi",
"version": "==0.2.0"
},
"requests": {
"hashes": [
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
],
"index": "pypi",
"version": "==2.24.0"
},
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"version": "==1.15.0"
},
"snowballstemmer": {
"hashes": [
"sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0",
"sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"
],
"version": "==2.0.0"
},
"sphinx": {
"hashes": [
"sha256:b4c750d546ab6d7e05bdff6ac24db8ae3e8b8253a3569b754e445110a0a12b66",
"sha256:fc312670b56cb54920d6cc2ced455a22a547910de10b3142276495ced49231cb"
],
"index": "pypi",
"version": "==2.4.4"
},
"sphinxcontrib-applehelp": {
"hashes": [
"sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a",
"sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"
],
"version": "==1.0.2"
},
"sphinxcontrib-devhelp": {
"hashes": [
"sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e",
"sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"
],
"version": "==1.0.2"
},
"sphinxcontrib-htmlhelp": {
"hashes": [
"sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f",
"sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"
],
"version": "==1.0.3"
},
"sphinxcontrib-jsmath": {
"hashes": [
"sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
"sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
],
"version": "==1.0.1"
},
"sphinxcontrib-qthelp": {
"hashes": [
"sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72",
"sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"
],
"version": "==1.0.3"
},
"sphinxcontrib-serializinghtml": {
"hashes": [
"sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc",
"sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"
],
"version": "==1.1.4"
},
"toml": {
"hashes": [
"sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
],
"version": "==0.10.1"
},
"typed-ast": {
"hashes": [
"sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
"sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
"sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
"sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
"sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
"sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
"sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
"sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
"sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
"sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
"sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
"sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
"sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
"sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
"sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
"sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
"sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
"sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
"sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
"sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
"sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
],
"markers": "implementation_name == 'cpython' and python_version < '3.8'",
"version": "==1.4.1"
},
"typing-extensions": {
"hashes": [
"sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5",
"sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae",
"sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"
],
"version": "==3.7.4.2"
},
"urllib3": {
"hashes": [
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
],
"version": "==1.25.10"
},
"wrapt": {
"hashes": [
"sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"
],
"version": "==1.12.1"
}
}
}
+38 -2
View File
@@ -1,5 +1,38 @@
.. image:: https://raw.githubusercontent.com/instaloader/instaloader/master/docs/logo_heading.png
.. badges-start
|travis| |pypi| |pyversion| |license| |aur| |contributors| |downloads|
.. |travis| image:: https://img.shields.io/travis/instaloader/instaloader/master.svg
:alt: Travis-CI Build Status
:target: https://travis-ci.org/instaloader/instaloader
.. |pypi| image:: https://img.shields.io/pypi/v/instaloader.svg
:alt: Instaloader PyPI Project Page
:target: https://pypi.org/project/instaloader/
.. |license| image:: https://img.shields.io/github/license/instaloader/instaloader.svg
:alt: MIT License
:target: https://github.com/instaloader/instaloader/blob/master/LICENSE
.. |pyversion| image:: https://img.shields.io/pypi/pyversions/instaloader.svg
:alt: Supported Python Versions
.. |contributors| image:: https://img.shields.io/github/contributors/instaloader/instaloader.svg
:alt: Contributor Count
:target: https://github.com/instaloader/instaloader/graphs/contributors
.. |aur| image:: https://img.shields.io/aur/version/instaloader.svg
:alt: Arch User Repository Package
:target: https://aur.archlinux.org/packages/instaloader/
.. |downloads| image:: https://pepy.tech/badge/instaloader/month
:alt: PyPI Download Count
:target: https://pepy.tech/project/instaloader/month
.. badges-end
::
$ pip3 install instaloader
@@ -17,11 +50,14 @@
directory accordingly,
- allows **fine-grained customization** of filters and where to store
downloaded media.
downloaded media,
- automatically **resumes previously-interrupted** download iterations.
::
instaloader [--comments] [--geotags] [--stories] [--highlights] [--tagged]
instaloader [--comments] [--geotags]
[--stories] [--highlights] [--tagged] [--igtv]
[--login YOUR-USERNAME] [--fast-update]
profile | "#hashtag" | :stories | :feed | :saved
+15
View File
@@ -0,0 +1,15 @@
pkgbase = instaloader
pkgdesc = Command line tool to download pictures, videos and metadata from Instagram
pkgver = {{version}}
pkgrel = 1
url = https://instaloader.github.io/
arch = any
license = MIT
makedepends = python-setuptools
depends = python>=3.5
depends = python-requests>=2.4
options = !emptydirs
source = instaloader-{{version}}.tar.gz::https://codeload.github.com/instaloader/instaloader/tar.gz/v{{version}}
sha512sums = {{hash}}
pkgname = instaloader
+27
View File
@@ -0,0 +1,27 @@
# Maintainer: André Koch-Kramer <koch-kramer@web.de>
pkgname=instaloader
pkgver={{version}}
pkgrel=1
pkgdesc="Command line tool to download pictures, videos and metadata from Instagram"
arch=('any')
url="https://instaloader.github.io/"
license=('MIT')
groups=()
depends=('python>=3.5' 'python-requests>=2.4')
makedepends=('python-setuptools')
options=('!emptydirs')
source=($pkgname-$pkgver.tar.gz::https://codeload.github.com/instaloader/instaloader/tar.gz/v$pkgver)
sha512sums=('{{hash}}')
build() {
cd "$srcdir/$pkgname-$pkgver"
python setup.py clean --all
python setup.py build
}
package() {
cd "$srcdir/$pkgname-$pkgver"
install -Dm644 LICENSE "$pkgdir"/usr/share/licenses/$pkgname/LICENSE
python setup.py install --root "$pkgdir/" --skip-build
}
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
VERSION=${1:1}
# do not deploy pre-releases
echo $VERSION | grep -qe "[abc]" && exit 0
cd $(dirname $0)
# decrypt and add ssh key
openssl aes-256-cbc -K $encrypted_354637631c28_key -iv $encrypted_354637631c28_iv -in id_rsa_AUR.enc -out /tmp/AUR_openssh -d
eval "$(ssh-agent -s)"
chmod 600 /tmp/AUR_openssh
ssh-add /tmp/AUR_openssh
# clone and modify AUR repo
git clone --depth 1 ssh://aur@aur.archlinux.org/instaloader.git
curl -sSfOJ https://codeload.github.com/instaloader/instaloader/tar.gz/$1
HASH=$(sha512sum instaloader-$VERSION.tar.gz | cut -f1 -d " ")
sed -e "s/{{version}}/$VERSION/g" -e "s/{{hash}}/$HASH/g" PKGBUILD.template > instaloader/PKGBUILD
sed -e "s/{{version}}/$VERSION/g" -e "s/{{hash}}/$HASH/g" .SRCINFO.template > instaloader/.SRCINFO
# commit and push changes
cd instaloader
git config user.email "koch-kramer@web.de"
git config user.name "André Koch-Kramer"
git add .
git commit -m "Release of version $VERSION"
git push
Binary file not shown.
+71
View File
@@ -0,0 +1,71 @@
import hashlib
import os
import re
import shutil
import subprocess
import sys
# copy the required files into repo root
shutil.copy('docs/favicon.ico', '.')
shutil.copy('deploy/windows/instaloader.spec', '.')
code = """
import contextlib
import psutil
import subprocess
def __main():
with contextlib.suppress(AttributeError, psutil.Error):
if psutil.Process().parent().parent().name() == "explorer.exe":
subprocess.Popen("powershell -NoExit -Command \\\"& '{0}'\\\"".format(sys.argv[0]))
return
main()
if __name__ == "__main__":
__main()
"""
with open('instaloader/__main__.py', 'r') as f:
# adjust imports for changed file structure
regex = re.compile(r'from (?:(\.[^ ]+ )|\.( ))import')
lines = [regex.sub(r'from instaloader\1\2import', line) for line in f.readlines()]
# insert code for magic exe behavior
index = lines.index('if __name__ == "__main__":\n')
code_lines = [cl + '\n' for cl in code.splitlines()]
for i, code_line in enumerate(code_lines):
if i + index < len(lines):
lines[i + index] = code_line
else:
lines.extend(code_lines[i:])
break
with open('__main__.py', 'w+') as f:
f.writelines(lines)
# install dependencies and invoke PyInstaller
commands = ["pip install pipenv==2020.6.2",
"pipenv sync --dev",
"pipenv run pyinstaller --log-level=DEBUG instaloader.spec"]
for command in commands:
print()
print('#' * (len(command) + 6))
print('## {} ##'.format(command))
print('#' * (len(command) + 6))
print(flush=True)
err = subprocess.Popen(command).wait()
if err != 0:
sys.exit(err)
# calculate and store MD5 hash for created executable
hash_md5 = hashlib.md5()
with open('dist/instaloader.exe', 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
hash_md5.update(chunk)
with open('dist/instaloader.exe.md5', 'w+') as f:
f.write('{} *instaloader.exe\n'.format(hash_md5.hexdigest()))
# Create ZIP file
shutil.make_archive('instaloader-{}-windows-standalone'.format(os.getenv('VERSION_TAG')), 'zip', 'dist')
+28
View File
@@ -0,0 +1,28 @@
# -*- mode: python -*-
block_cipher = None
a = Analysis(['__main__.py'],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='instaloader',
debug=False,
strip=False,
upx=False,
runtime_tmpdir=None,
console=True , icon='favicon.ico')
File diff suppressed because one or more lines are too long
+50
View File
File diff suppressed because one or more lines are too long
-28
View File
@@ -1,28 +0,0 @@
code {
color: #222;
background-color: #f5f5f5;
}
dd {
padding-left: 8px;
}
dl.option dd {
padding-left: 12px;
}
a {
color: #008d06;
}
a:hover {
color: #f48400;
}
body {
font-size: 16px;
}
.versionmodified {
font-size: 14px;
}
-14
View File
@@ -1,14 +0,0 @@
<li class="dropdown globaltoc-container">
<a role="button"
id="dLabelGlobalToc"
data-toggle="dropdown"
data-target="#"
href="{{ pathto(master_doc) }}">{{ theme_navbar_site_name }} <b class="caret"></b></a>
<ul class="dropdown-menu globaltoc"
role="menu"
aria-labelledby="dLabelGlobalToc">
{{ toctree(maxdepth=theme_globaltoc_depth|toint, collapse=False, includehidden=theme_globaltoc_includehidden|tobool) }}
<li role="separator" class="divider"></li>
<li class="toctree-l1"><a class="reference internal" href="genindex.html">Index</a></li>
</ul>
</li>
+72 -7
View File
@@ -1,18 +1,83 @@
{% extends "!layout.html" %}
{% set html5_doctype = True %}
{% set script_files = script_files + ['_static/instaloaderdoc.js'] %}
{% block htmltitle %}
{% if pagename == "index" %}
<title>{{ title|striptags|e }}</title>
<title>{{ title|striptags|e }}</title>
{% else %}
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
{% endif %}
{% endblock %}
{% block extrahead %}
{{ super() }}
<meta name="google-site-verification" content="FNUsDdkOJM9obm9QEdrNBVd9AtAzHzA-aaSMCUrS6C8" />
{{ super() }}
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="google-site-verification" content="FNUsDdkOJM9obm9QEdrNBVd9AtAzHzA-aaSMCUrS6C8"/>
{% if pagename == "index" %}
<link rel="canonical" href="https://instaloader.github.io/" />
<link rel="canonical" href="https://instaloader.github.io/"/>
{% else %}
<link rel="canonical" href="https://instaloader.github.io/{{ pagename }}.html" />
<link rel="canonical" href="https://instaloader.github.io/{{ pagename }}.html"/>
{% endif %}
<script async defer src="https://buttons.github.io/buttons.js"></script>
{% endblock %}
{% block header %}
<nav class="navbar navbar-light navbar-expand bg-light border-bottom">
<a class="navbar-brand" href="{{ pathto(master_doc) }}">
<img src="{{ pathto('_static/' + logo, 1) }}" width="30" height="30" class="d-inline-block align-top" alt="logo">
<span class="ml-3">Instaloader</span>
</a>
<ul class="navbar-nav mr-auto">
<li class="nav-item ml-0 ml-sm-2">
<a class="nav-link"
href="https://github.com/instaloader/instaloader/releases/tag/v{{ current_release }}"
title="Released on {{ current_release_date }}" data-toggle="tooltip">
v<b>{{ current_release }}</b>
</a>
</li>
</ul>
<ul class="navbar-nav ml-auto d-none d-sm-flex">
<li class="nav-item">
<a class="nav-link" href="https://github.com/instaloader/instaloader/issues">
Issues
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/instaloader/instaloader/releases">
Releases
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/instaloader/instaloader">
GitHub
</a>
</li>
</ul>
<button class="d-inline-block d-md-none btn btn-outline-primary btn-sm" type="button"
data-toggle="collapse" data-target="#instaloaderdocSidebar" aria-controls="instaloaderdocSidebar"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</nav>
{% endblock %}
{% block relbar1 %}{% endblock %}
{% block relbar2 %}{% endblock %}
{% block footer %}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="instaloaderdoc-sidebar collapse d-md-block col-md-3 col-xl-2 bg-light"
id="instaloaderdocSidebar">
<nav role="navigation" aria-label="Table of Contents">
<form action="{{ pathto('search') }}" method="get" role="search" class="border-bottom mr-2 pb-2 mb-2 px-1">
<label class="sr-only" for="searchQueryInput">Search</label>
<input type="search" name="q" id="searchQueryInput" class="form-control my-2"
placeholder="Search..." aria-label="Search">
</form>
<h4>Contents</h4>
{{ toctree(maxdepth=-1) }}
</nav>
</div>
<main class="col-12 col-md-9 col-xl-10 pt-2" role="main">
{% block body %}{% endblock %}
</main>
</div>
</div>
{% endblock %}
-60
View File
@@ -1,60 +0,0 @@
<div id="navbar" class="{{ theme_navbar_class }} navbar-default {% if theme_navbar_fixed_top|tobool -%} navbar-fixed-top{%- endif -%}">
<div class="container">
<div class="navbar-header">
<!-- .btn-navbar is used as the toggle for collapsed navbar content -->
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{ pathto(master_doc) }}">
{%- block sidebarlogo %}
{%- if logo %}<span><img src="{{ pathto('_static/' + logo, 1) }}"></span>{%- endif %}
{%- endblock %}
{% if theme_navbar_title -%}{{ theme_navbar_title|e }}{%- else -%}{{ project|e }}{%- endif -%}
</a>
<span class="navbar-text navbar-version pull-left">
<a href="https://github.com/instaloader/instaloader/releases/tag/v{{current_release}}" title="Released on {{current_release_date}}">v<b>{{current_release}}</b></a>
</span>
</div>
<div class="collapse navbar-collapse nav-collapse">
<ul class="nav navbar-nav">
{% if theme_navbar_links %}
{%- for link in theme_navbar_links %}
<li><a href="{{ pathto(*link[1:]) }}">{{ link[0] }}</a></li>
{%- endfor %}
{% endif %}
{% block navbartoc %}
{% include "globaltoc.html" %}
{% if theme_navbar_pagenav and pagename not in ("index", "genindex", "installation") %}
{% include "navbartoc.html" %}
{% endif %}
{% endblock %}
{% if theme_navbar_sidebarrel %}
{% block sidebarrel %}
{% include "relations.html" %}
{% endblock %}
{% endif %}
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a role="button" id="dLabelUsefulLinks" data-toggle="dropdown" data-target="#" href="#">
Useful Links
<b class="caret"></b>
</a>
<ul class="dropdown-menu" role="menu" aria-labelledby="dLabelUsefulLinks">
<li><a href="https://github.com/instaloader/instaloader/issues">Issue Tracker</a></li>
<li><a href="https://github.com/instaloader/instaloader/releases">Version History</a></li>
<li><a href="https://pypi.org/project/instaloader/">Instaloader on PyPI</a></li>
<li><a href="https://github.com/instaloader/instaloader">Instaloader on GitHub</a></li>
<li role="separator" class="divider"></li>
<li style="padding-left: 20px;"><a class="github-button" href="https://github.com/instaloader/instaloader" data-icon="octicon-star" data-size="large" data-show-count="true" aria-label="Star Instaloader on GitHub">Star</a></li>
<li role="separator" class="divider"></li>
<li><a href="https://www.paypal.me/aandergr"><b>Donate (via Paypal)</b></a></li>
</ul>
</li>
</ul>
</div>
</div>
</div>
+28
View File
@@ -0,0 +1,28 @@
{% extends "!search.html" %}
{% block body %}
<div id="fallback" class="admonition warning">
<script type="text/javascript">$('#fallback').hide();</script>
<p>
{% trans %}Please activate JavaScript to enable the search
functionality.{% endtrans %}
</p>
</div>
{% if search_performed %}
<h1>{{ _('Search Results') }}</h1>
{% if not search_results %}
<p>{{ _('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\'ve selected enough categories.') }}</p>
{% endif %}
{% endif %}
<div id="search-results">
{% if search_results %}
<ul>
{% for href, caption, context in search_results %}
<li><a href="{{ pathto(item.href) }}">{{ caption }}</a>
<div class="context">{{ context|e }}</div>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endblock %}
+16 -112
View File
@@ -12,11 +12,8 @@ Python Module :mod:`instaloader`
.. highlight:: python
.. contents::
:backlinks: none
Instaloader exposes its internally used methods and structures as a Python
module, making it a **powerful and intuitive Python API for Instagram**,
module, making it a powerful and intuitive Python API for Instagram,
allowing to further customize obtaining media and metadata.
Start with getting an instance of :class:`Instaloader`::
@@ -37,7 +34,7 @@ picture, video or sidecar (set of multiple pictures/videos) posted in a user's
profile. :class:`Instaloader` provides methods to iterate over Posts from a
certain source::
for post in L.get_hashtag_posts('cat'):
for post in instaloader.Hashtag.from_name(L.context, 'cat').get_posts():
# post is an instance of instaloader.Post
L.download_post(post, target='#cat')
@@ -63,7 +60,7 @@ certain source::
- :meth:`Instaloader.get_explore_posts`
Media that is suggested by Instagram to explore.
- :meth:`Instaloader.get_hashtag_posts`
- :meth:`Hashtag.get_posts`
Media associated with given hashtag.
With the :class:`Profile` class, Instaloader also makes it easy to access
@@ -85,119 +82,26 @@ metadata of a Profile. :class:`Profile` instances can be created with:
- :meth:`Profile.get_followers`
Profiles that follow given user.
- :attr:`Post.owner_profile`, :attr:`Story.owner_profile` and :attr:`StoryItem.owner_profile`
- :attr:`Post.owner_profile`, :attr:`Story.owner_profile`, :attr:`StoryItem.owner_profile`
Owner profile of particular object.
- :meth:`Post.get_likes`
Profiles that liked a given :class:`Post`
- :attr:`PostComment.owner` attribute for comment in :meth:`Post.get_comments`
- :attr:`PostComment.owner`
Profile of a Post comment.
A reference of the many methods provided by the :mod:`instaloader` module is
provided in the remainder of this document.
For a list of a few code examples that use the Instaloader module, see
:ref:`codesnippets`.
For a list of real code examples using the Instaloader module for advanced
tasks, see :ref:`codesnippets`.
The reference of the classes and functions provided by the :mod:`instaloader` module is
divided into the following subsections:
``Instaloader`` (Main Class)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. toctree::
:maxdepth: 2
.. autoclass:: Instaloader
:no-show-inheritance:
Instagram Structures
^^^^^^^^^^^^^^^^^^^^
Posts
"""""
.. autoclass:: Post
:no-show-inheritance:
Additionally, the following trivial structures are defined:
.. autoclass:: PostSidecarNode
:no-show-inheritance:
.. autoclass:: PostComment
:no-show-inheritance:
.. autoclass:: PostLocation
:no-show-inheritance:
User Stories
""""""""""""
.. autoclass:: Story
:no-show-inheritance:
.. autoclass:: StoryItem
:no-show-inheritance:
Highlights
""""""""""
.. autoclass:: Highlight
:no-show-inheritance:
:inherited-members:
Bases: :class:`Story`
.. versionadded:: 4.1
Profiles
""""""""
.. autoclass:: Profile
:no-show-inheritance:
Loading and Saving
""""""""""""""""""
:class:`Post`, :class:`StoryItem` and :class:`Profile` can be saved and loaded
to/from JSON files.
.. autofunction:: load_structure_from_file
.. autofunction:: save_structure_to_file
Exceptions
^^^^^^^^^^
.. currentmodule:: instaloader.exceptions
.. autoexception:: InstaloaderException
:no-show-inheritance:
.. autoexception:: ConnectionException
.. currentmodule:: instaloader
.. autoexception:: QueryReturnedBadRequestException
.. autoexception:: QueryReturnedNotFoundException
.. autoexception:: QueryReturnedForbiddenException
.. autoexception:: ProfileNotExistsException
.. autoexception:: ProfileHasNoPicsException
.. autoexception:: PrivateProfileNotFollowedException
.. autoexception:: LoginRequiredException
.. autoexception:: InvalidArgumentException
.. autoexception:: BadResponseException
.. autoexception:: BadCredentialsException
.. autoexception:: TooManyRequestsException
``InstaloaderContext`` (Low-level functions)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. autoclass:: InstaloaderContext
:no-show-inheritance:
module/instaloader
module/structures
module/nodeiterator
module/instaloadercontext
module/exceptions
+42 -25
View File
@@ -51,7 +51,7 @@ invoke it with
instaloader --login=your_username profile [profile ...]
When logging in, Instaloader **stores the session cookies** in a file in your
temporary directory, which will be reused later the next time :option:`--login`
home directory, which will be reused later the next time :option:`--login`
is given. So you can download private profiles **non-interactively** when you
already have a valid session cookie file.
@@ -77,31 +77,46 @@ Instaloader supports the following targets:
instructs Instaloader to also **download the user's stories**,
- :option:`--highlights`
to **download highlights of each profile that is downloaded**, and
to **download the highlights of that profile**,
- :option:`--tagged`
to **download posts where the user is tagged**.
to **download posts where the user is tagged**, and
- :option:`--igtv`
to **download IGTV videos**.
- ``"#hashtag"``
Posts with a certain **hashtag** (the quotes are usually necessary),
Posts with a certain **hashtag** (the quotes are usually necessary).
- ``%location id``
Posts tagged with a given location; the location ID is the numerical ID
Instagram labels a location with (e.g.
\https://www.instagram.com/explore/locations/**362629379**/plymouth-naval-memorial/).
Requires :option:`--login`.
.. versionadded:: 4.2
- ``:stories``
The currently-visible **stories** of your followees (requires
:option:`--login`),
:option:`--login`).
- ``:feed``
Your **feed** (requires :option:`--login`),
Your **feed** (requires :option:`--login`).
- ``:saved``
Posts which are marked as **saved** (requires :option:`--login`),
Posts which are marked as **saved** (requires :option:`--login`).
- ``@profile``
All profiles that are followed by ``profile``, i.e. the *followees* of
``profile`` (requires :option:`--login`).
- ``-post``
The single **post** with the given shortcode. Must be preceeded by ``--`` in
the argument list to not be mistaken as an option flag.
Replace **post** with the post's shortcode to download single post. Must be preceded by ``--`` in
the argument list to not be mistaken as an option flag::
instaloader -- -B_K4CykAOtf
.. versionadded:: 4.1
@@ -115,7 +130,7 @@ downloads the pictures and videos and their captions. You can specify
- :option:`--geotags`
**download geotags** of each post and save them as
Google Maps link,
Google Maps link (requires :option:`--login`),
For a reference of all supported command line options, see
:ref:`command-line-options`.
@@ -126,7 +141,7 @@ Filename Specification
^^^^^^^^^^^^^^^^^^^^^^
For each target, Instaloader creates a directory named after the target,
i.e. ``profile``, ``#hashtag``, ``:feed``, etc. and therein saves the
i.e. ``profile``, ``#hashtag``, ``%location id``, ``:feed``, etc. and therein saves the
posts in files named after the post's timestamp.
:option:`--dirname-pattern` allows to configure the directory name of each
@@ -181,7 +196,7 @@ Filter Posts
.. py:currentmodule:: instaloader
The options :option:`--post-filter` and :option:`--storyitem-filter`
allows to specify criteria that posts or story items have to
allow to specify criteria that posts or story items have to
meet to be downloaded. If not given, all posts are downloaded.
The filter string must be a
@@ -193,7 +208,7 @@ Id est, the following attributes can be used with both
:option:`--post-filter` and :option:`--storyitem-filter`:
- :attr:`~Post.owner_username` (str), :attr:`~Post.owner_id` (int)
Owner profile username / userid.
Owner profile username / user ID.
- :attr:`~Post.date_utc` (datetime), :attr:`~Post.date_local` (datetime)
Creation timestamp. Since :class:`~datetime.datetime` objects can be created
@@ -202,7 +217,7 @@ Id est, the following attributes can be used with both
instaloader --post-filter="date_utc <= datetime(2018, 5, 31)" target
- :attr:`~Post.is_video` (bool)
Post/StoryItem is a video. For example, you may skip videos::
Whether Post/StoryItem is a video. For example, you may skip videos::
instaloader --post-filter="not is_video" target
@@ -219,8 +234,8 @@ As :option:`--post-filter`, the following attributes can be used additionally:
instaloader --login=your_username --post-filter=viewer_has_liked :feed
- :attr:`~Post.likes` (int), :attr:`~Post.comments` (int)
Likes count / Comments count. You might only want to download posts that
either you liked or were liked by many others**::
Likes count / comments count. You might only want to download posts that
were either liked by yourself or by many others::
instaloader --login=your_username --post-filter="likes>100 or viewer_has_liked" profile
@@ -250,7 +265,7 @@ along with each post where the Post's caption is saved.
You can customize what metadata to save for each Post or StoryItem with
:option:`--post-metadata-txt` and :option:`--storyitem-metadata-txt`. The
default is ``--post-metadata-txt={caption}`` and no storyitem metadata txt.
These strings are formatted similar as in the :ref:`filename-specification` and
These strings are formatted similar as the path patterns described in :ref:`filename-specification` and
the result is saved in text files, unless it is empty.
Specifying these options multiple times results in output having multiple lines,
@@ -280,17 +295,18 @@ 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 logged-in, create a session file::
instaloader --login=your_username --sessionfile=~/.instaloader-session
instaloader --login=your_username
Then use the same parameters in your cronjob to load the session and download
Then use the same username in your cronjob to load the session and download
the given targets::
instaloader --login=your_username --sessionfile=~/.instaloader-session --quiet <target> [...]
instaloader --login=your_username --quiet target [...]
Without :option:`--sessionfile` option, Instaloader saves the session file in
a path within your temporary directory.
Instaloader saves the session file to
``~/.config/instaloader/session-YOUR-USERNAME``. See
:option:`--sessionfile` option for how to override this path.
Programming Instaloader
^^^^^^^^^^^^^^^^^^^^^^^
@@ -301,5 +317,6 @@ 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.
Also see :ref:`codesnippets`, where we collect example scripts that use
Instaloader to achieve more complex tasks.
Also see :ref:`codesnippets`, where we collect a few example scripts that use
Instaloader for simple tasks that cannot be done with the command line
interface.
+52 -38
View File
@@ -8,7 +8,7 @@ Instaloader is invoked with::
$ instaloader [options] target [target ...]
where ``target`` is a ``profile``, a ``"#hashtag"``, ``@profile`` (all profiles
that *profile* is following), or if logged in ``:feed`` (pictures from your
that *profile* is following), ``%location ID``, or if logged in ``:feed`` (pictures from your
feed), ``:stories`` (stories of your followees) or ``:saved`` (collection of
posts marked as saved).
@@ -36,7 +36,7 @@ stores all posts along with the pictures's captions there.
Re-Download the given object
- ``+args.txt``
Read targets (and options) from given textfile. See :option:`+args.txt`.
Read targets (and options) from given text file. See :option:`+args.txt`.
What to Download of each Post
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -59,10 +59,9 @@ What to Download of each Post
.. option:: --geotags, -G
**Download geotags** when available. Geotags are stored as a text file with
Download geotags when available. Geotags are stored as a text file with
the location's name and a Google Maps link. This requires an additional
request to the Instagram server for each picture, which is why it is disabled
by default.
request to the Instagram server for each picture. Requires :option:`--login`.
.. option:: --comments, -C
@@ -95,13 +94,6 @@ What to Download of each Post
What to Download of each Profile
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. option:: --profile-pic-only, -P
.. deprecated:: 4.1
Use :option:`--no-posts`.
Only download profile picture.
.. option:: --no-posts
Do not download regular posts.
@@ -114,12 +106,12 @@ What to Download of each Profile
.. option:: --stories, -s
Also **download stories** of each profile that is downloaded. Requires
Also download stories of each profile that is downloaded. Requires
:option:`--login`.
.. option:: --highlights
Also **download highlights** of each profile that is downloaded. Requires
Also download highlights of each profile that is downloaded. Requires
:option:`--login`.
.. versionadded:: 4.1
@@ -130,19 +122,11 @@ What to Download of each Profile
.. versionadded:: 4.1
.. option:: --stories-only
.. option:: --igtv
.. deprecated:: 4.1
Use :option:`--stories` :option:`--no-posts`.
Also download IGTV videos.
Rather than downloading regular posts of each specified profile, only
download stories. Requires :option:`--login`. Does not imply
:option:`--no-profile-pic`.
.. note::
If possible, use ``:stories`` target rather than :option:`--stories-only`
with all your followees. ``:stories`` uses fewer API requests.
.. versionadded:: 4.3
Which Posts to Download
^^^^^^^^^^^^^^^^^^^^^^^
@@ -171,13 +155,13 @@ Which Posts to Download
.. option:: --count COUNT, -c
Do not attempt to download more than COUNT posts. Applies only to
``#hashtag`` and ``:feed``.
``#hashtag``, ``%location id``, and ``:feed``.
Login (Download Private Profiles)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Instaloader can **login to Instagram**. This allows downloading private
Instaloader can login to Instagram. This allows downloading private
profiles. To login, pass the :option:`--login` option. Your session cookie (not your
password!) will be saved to a local file to be reused next time you want
Instaloader to login.
@@ -188,9 +172,8 @@ Instaloader to login.
.. option:: --sessionfile SESSIONFILE, -f SESSIONFILE
Path for loading and storing session key file. Defaults to a path within
your temporary directory, encoding your local username and your Instagram
profile name.
Path for loading and storing session key file. Defaults to
``~/.config/instaloader/session-YOUR-USERNAME``.
.. option:: --password YOUR-PASSWORD, -p YOUR-PASSWORD
@@ -219,6 +202,29 @@ How to Download
``#hashtag`` or the profile name. Defaults to ``{date_utc}_UTC``.
See :ref:`filename-specification` for a list of supported tokens.
.. option:: --resume-prefix prefix
For many targets, Instaloader is capable of resuming a previously-aborted
download loop. To do so, it creates a JSON file within the target directory
when interrupted. This option controls the prefix for filenames that are
used to save the information to resume an interrupted download. The default
prefix is ``iterator``.
Resuming an interrupted download is supported for most, but not all targets.
JSON files with resume information are always compressed, regardless of
:option:`--no-compress-json`.
This feature is turned off entirely with :option:`--no-resume`.
.. versionadded:: 4.5
.. option:: --no-resume
Do not resume a previously-aborted download iteration, and do not save such
information when interrupted.
.. versionadded:: 4.5
.. option:: --user-agent USER_AGENT
User Agent to use for HTTP requests. Per default, Instaloader pretends being
@@ -230,6 +236,12 @@ How to Download
to ``3``. If a connection fails, it can be manually skipped by hitting
:kbd:`Control-c`. Set this to ``0`` to retry infinitely.
.. option:: --request-timeout N
Seconds to wait before timing out a connection request.
.. versionadded:: 4.3
Miscellaneous Options
^^^^^^^^^^^^^^^^^^^^^
@@ -241,18 +253,20 @@ Miscellaneous Options
.. option:: +args.txt
Read arguments from file `args.txt`, a shortcut to provide argument from
file rather than command-line. This provide a convient way to hide login
info from CLI. and also can use for simplify managment of long arguments.
Read arguments from file `args.txt`, a shortcut to provide arguments from
file rather than command-line. This provides a convenient way to hide login
info from CLI, and can also be used to simplify management of long arguments.
.. note::
text file should separate arg with line break.
Text file should separate arguments with line breaks.
args.txt example::
args.txt example::
--login MYUSENAME
--password MYPASSWORD
--fast-update
--login=MYUSERNAME
--password=MYPASSWORD
--fast-update
profile1
profile2
.. versionadded:: 4.1
+22 -29
View File
@@ -28,8 +28,9 @@ Download Posts in a Specific Period
-----------------------------------
To only download Instagram pictures (and metadata) that are within a specific
period, you can play around with :func:`~itertools.dropwhile` and
:func:`~itertools.takewhile` from :mod:`itertools` like in this snippet.
period, you can simply use :func:`~itertools.dropwhile` and
:func:`~itertools.takewhile` from :mod:`itertools` on a generator that returns
Posts in **exact chronological order**, such as :meth:`Profile.get_posts`.
.. literalinclude:: codesnippets/121_since_until.py
@@ -37,11 +38,19 @@ See also :class:`Post`, :meth:`Instaloader.download_post`.
Discussed in :issue:`121`.
The code example with :func:`~itertools.dropwhile` and
:func:`~itertools.takewhile` makes the assumption that the post iterator returns
posts in exact chronological order. As discussed in :issue:`666`, the following
approach fits for an **almost chronological order**, where up to *k* older posts
are inserted into an otherwise chronological order, such as a Hashtag feed.
.. literalinclude:: codesnippets/666_historical_hashtag_data.py
Likes of a Profile / Ghost Followers
------------------------------------
To obtain a list of your inactive followers, i.e. followers that did not like
any of your pictures, into a file you can use this approach.
any of your pictures, you can use this approach.
.. literalinclude:: codesnippets/120_ghost_followers.py
@@ -69,11 +78,11 @@ Discussed in :issue:`56`.
Only one Post per User
----------------------
To download only the one most recent post from each user, this snippet creates a
:class:`set` that contains the users of which a post has already been
downloaded. While iterating the posts, it checks whether the post's owner
already is in the set. If not, the post is downloaded from Instagram and the
user is added to that set.
To download only the single most recent post per user within a hashtag feed,
this snippet uses a :class:`set` that contains the users of whom a post has
already been downloaded. For each post, it checks whether the post's creator is
already contained in that set. If not, the post is downloaded from Instagram and
the user is added to that set.
.. literalinclude:: codesnippets/113_only_one_per_user.py
@@ -82,30 +91,14 @@ See also :class:`Post`, :meth:`Instaloader.download_post`,
Discussed in :issue:`113`.
Upgrade Images by Local Copies
------------------------------
Top X Posts of User
-------------------
The following script finds local versions of images fetched by Instaloader, in
order to upgrade the downloaded images by locally-found versions with better
quality. It uses image hashing to identify similar images.
With Instaloader, it is easy to download the few most-liked pictures of a user.
`updgrade-instaloader-images.py <https://gist.github.com/pavelkryukov/15f93d19a99428a284a8bcec27e0187b>`__ (external link to GitHub Gist)
.. literalinclude:: codesnippets/194_top_x_of_user.py
Discussed in :issue:`46`.
Add Captions to Images
----------------------
Instaloader does not modify the downloaded JPEG file. However, one could combine
it with an imaging library such as Pillow or PIL to render the caption on
Instagram pictures. The following shows an approach.
.. literalinclude:: codesnippets/110_pil_captions.py
See also :attr:`Post.caption`, :attr:`Post.url`, :meth:`Post.from_shortcode`,
:func:`load_structure_from_file`.
Discussed in :issue:`110`.
Discussed in :issue:`194`.
Metadata JSON Files
-------------------
-20
View File
@@ -1,20 +0,0 @@
from io import BytesIO
from requests import get
from PIL import Image, ImageDraw
from instaloader import *
L = Instaloader()
# Load Post instance
post = load_structure_from_file(L.context, '2017-10-01_18-53-03_UTC.json.xz')
# or post = Post.from_shortcode(L.context, SHORTCODE)
# Render caption
image = Image.open(BytesIO(get(post.url).content))
draw = ImageDraw.Draw(image)
color = 'rgb(0, 0, 0)' # black color
draw.text((300,100), post.caption.encode('latin1', errors='ignore'), fill=color)
# Save image
image.save('test.jpg')
+2 -2
View File
@@ -2,13 +2,13 @@ import instaloader
L = instaloader.Instaloader()
posts = L.get_hashtag_posts('milfgarden')
posts = instaloader.Hashtag.from_name(L.context, 'urbanphotography').get_posts()
users = set()
for post in posts:
if not post.owner_profile in users:
L.download_post(post, '#milfgarden')
L.download_post(post, '#urbanphotography')
users.add(post.owner_profile)
else:
print("{} from {} skipped.".format(post, post.owner_profile))
+6 -6
View File
@@ -2,26 +2,26 @@ import instaloader
L = instaloader.Instaloader()
USER = 'your_account'
USER = "your_account"
PROFILE = USER
# Your preferred way of logging in:
# Load session previously saved with `instaloader -l USERNAME`:
L.load_session_from_file(USER)
profile = instaloader.Profile.from_username(L.context, PROFILE)
likes = set()
print('Fetching likes of all posts of profile {}.'.format(profile.username))
print("Fetching likes of all posts of profile {}.".format(profile.username))
for post in profile.get_posts():
print(post)
likes = likes | set(post.get_likes())
print('Fetching followers of profile {}.'.format(profile.username))
print("Fetching followers of profile {}.".format(profile.username))
followers = set(profile.get_followers())
ghosts = followers - likes
print('Storing ghosts into file.')
with open('/YOUR PATH/inactive-users.txt', 'w') as f:
print("Storing ghosts into file.")
with open("inactive-users.txt", 'w') as f:
for ghost in ghosts:
print(ghost.username, file=f)
+2 -4
View File
@@ -5,13 +5,11 @@ import instaloader
L = instaloader.Instaloader()
posts = L.get_hashtag_posts('milfgarden')
# or
# posts = instaloader.Profile.from_username(L.context, PROFILE).get_posts()
posts = instaloader.Profile.from_username(L.context, "instagram").get_posts()
SINCE = datetime(2015, 5, 1)
UNTIL = datetime(2015, 3, 1)
for post in takewhile(lambda p: p.date > UNTIL, dropwhile(lambda p: p.date > SINCE, posts)):
print(post.date)
L.download_post(post, '#milfgarden')
L.download_post(post, "instagram")
+17
View File
@@ -0,0 +1,17 @@
from itertools import islice
from math import ceil
from instaloader import Instaloader, Profile
PROFILE = ... # profile to download from
X_percentage = 10 # percentage of posts that should be downloaded
L = Instaloader()
profile = Profile.from_username(L.context, PROFILE)
posts_sorted_by_likes = sorted(profile.get_posts(),
key=lambda p: p.likes + p.comments,
reverse=True)
for post in islice(posts_sorted_by_likes, ceil(profile.mediacount * X_percentage / 100)):
L.download_post(post, PROFILE)
@@ -0,0 +1,53 @@
from argparse import ArgumentParser
from glob import glob
from os.path import expanduser
from platform import system
from sqlite3 import OperationalError, connect
try:
from instaloader import ConnectionException, Instaloader
except ModuleNotFoundError:
raise SystemExit("Instaloader not found.\n pip install [--user] instaloader")
def get_cookiefile():
default_cookiefile = {
"Windows": "~/AppData/Roaming/Mozilla/Firefox/Profiles/*/cookies.sqlite",
"Darwin": "~/Library/Application Support/Firefox/Profiles/*/cookies.sqlite",
}.get(system(), "~/.mozilla/firefox/*/cookies.sqlite")
cookiefiles = glob(expanduser(default_cookiefile))
if not cookiefiles:
raise SystemExit("No Firefox cookies.sqlite file found. Use -c COOKIEFILE.")
return cookiefiles[0]
def import_session(cookiefile, sessionfile):
print("Using cookies from {}.".format(cookiefile))
conn = connect(cookiefile)
try:
cookie_data = conn.execute(
"SELECT name, value FROM moz_cookies WHERE baseDomain='instagram.com'"
)
except OperationalError:
cookie_data = conn.execute(
"SELECT name, value FROM moz_cookies WHERE host LIKE '%instagram.com'"
)
instaloader = Instaloader(max_connection_attempts=1)
instaloader.context._session.cookies.update(cookie_data)
username = instaloader.test_login()
if not username:
raise SystemExit("Not logged in. Are you logged in successfully in Firefox?")
print("Imported session cookie for {}.".format(username))
instaloader.context.username = username
instaloader.save_session_to_file(sessionfile)
if __name__ == "__main__":
p = ArgumentParser()
p.add_argument("-c", "--cookiefile")
p.add_argument("-f", "--sessionfile")
args = p.parse_args()
try:
import_session(args.cookiefile or get_cookiefile(), args.sessionfile)
except (ConnectionException, OperationalError) as e:
raise SystemExit("Cookie import failed: {}".format(e))
@@ -0,0 +1,31 @@
from datetime import datetime
import instaloader
L = instaloader.Instaloader()
posts = instaloader.Hashtag.from_name(L.context, "urbanphotography").get_posts()
SINCE = datetime(2020, 5, 10) # further from today, inclusive
UNTIL = datetime(2020, 5, 11) # closer to today, not inclusive
k = 0 # initiate k
#k_list = [] # uncomment this to tune k
for post in posts:
postdate = post.date
if postdate > UNTIL:
continue
elif postdate <= SINCE:
k += 1
if k == 50:
break
else:
continue
else:
L.download_post(post, "#urbanphotography")
# if you want to tune k, uncomment below to get your k max
#k_list.append(k)
k = 0 # set k to 0
#max(k_list)
+28 -28
View File
@@ -18,10 +18,10 @@
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import platform
import subprocess
import sys
import sphinx_bootstrap_theme
import typing
sys.path.insert(0, os.path.abspath('..'))
@@ -48,9 +48,27 @@ autodoc_default_flags = ['show-inheritance', 'members', 'undoc-members']
autodoc_member_order = 'bysource'
intersphinx_mapping = {'python': ('https://docs.python.org/3', None),
'requests': ('http://docs.python-requests.org/en/master', None)}
'requests': ('https://requests.kennethreitz.org/en/master/', None)}
extlinks = {'issue': ('https://github.com/instaloader/instaloader/issues/%s', 'issue #')}
nitpick_ignore = [('py:class', 'typing.Tuple')]
current_release = subprocess.check_output(["git", "describe", "--abbrev=0"]).decode("ascii")[1:-1]
date_format = "%e %b %Y" if platform.system() != "Windows" else "%d %b %Y"
current_release_date = subprocess.check_output(
["git", "log", "-1", "--tags", "--format=%ad", "--date=format:"+date_format]).decode("ascii")[:-1]
html_context = {'current_release': current_release, 'current_release_date': current_release_date}
extlinks = {
'issue': (
'https://github.com/instaloader/instaloader/issues/%s',
'Issue #'
),
'example': (
'https://raw.githubusercontent.com/instaloader/instaloader/v' + current_release + '/docs/codesnippets/%s',
'Example '
),
}
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
@@ -70,7 +88,7 @@ master_doc = 'index'
# General information about the project.
project = 'Instaloader'
copyright = '2017, Alexander Graf and Andre Koch-Kramer'
copyright = '2017-2020, Alexander Graf and Andre Koch-Kramer'
author = 'Alexander Graf and Andre Koch-Kramer'
# The version info for the project you're documenting, acts as replacement for
@@ -87,7 +105,7 @@ author = 'Alexander Graf and Andre Koch-Kramer'
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = None
language = "en"
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
@@ -123,7 +141,7 @@ add_module_names = False
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = 'fruity'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
@@ -140,20 +158,7 @@ todo_include_todos = False
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'bootstrap'
html_theme_path = sphinx_bootstrap_theme.get_html_theme_path()
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {
'navbar_site_name': 'Site Contents',
'navbar_pagenav_name': 'Page Contents',
'navbar_pagenav': True,
'navbar_sidebarrel': True,
'nosidebar': True,
}
html_theme = 'basic'
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
@@ -367,13 +372,8 @@ texinfo_documents = [
# texinfo_no_detailmenu = False
current_release = subprocess.check_output(["git", "describe", "--abbrev=0"]).decode("ascii")[1:-1]
current_release_date = subprocess.check_output(["git", "log", "-1", "--tags", "--format=%ad", "--date=format:%e %b %Y"]).decode("ascii")[:-1]
html_context = {'current_release': current_release, 'current_release_date': current_release_date}
def setup(app):
app.add_stylesheet("style.css")
typing.TYPE_CHECKING = True
app.add_stylesheet("instaloaderdoc.css")
app.connect('autodoc-process-signature', sphinx_autodoc_typehints.process_signature)
app.connect('autodoc-process-docstring', sphinx_autodoc_typehints.process_docstring)
+41 -27
View File
@@ -15,14 +15,14 @@ Reporting Bugs
--------------
If you encounter a bug, do not hesitate to report it in our
`Issue Tracker <https://github.com/instaloader/instaloader/issues>`__. Since
Instaloader is actively developed, the majority of bugs is fixed within only
**3 days** after being reported. When reporting a problem, please keep the
following in mind:
`Issue Tracker <https://github.com/instaloader/instaloader/issues>`__. When
reporting a problem, please keep the following in mind:
- Ensure you **use the latest version** of Instaloader. The currently-installed
version can be found out with ``instaloader --version``.
- Check whether there is a valid solution in our :ref:`troubleshooting` section.
- Briefly **check whether the bug has already been reported**. If you find an
issue reporting the same bug you encountered, comment there rather than
opening a new issue. However, if unsure, please create a new issue.
@@ -32,11 +32,11 @@ following in mind:
- Include all **error messages and tracebacks** in the report.
- If not obvious, describe **which behavior you expected**
- Even if it seems obvious, describe **which behavior you expected**
instead of what actually happened.
- If you think an issue has been closed accidentally or inappropriately, feel
free to reopen it.
- If we have closed an issue apparently inadvertently or inappropriately, please
let us know.
Writing Code or Improving the Documentation
-------------------------------------------
@@ -45,29 +45,35 @@ Changes of the Instaloader source can be proposed as a
`Pull Request <https://github.com/instaloader/instaloader/pulls>`__. There are only
few things to consider:
- Sometimes, the most current code is not in the ``master`` branch. Check that
you forked from the most recent branch.
- Base your Pull Request on the ``master`` branch if it fixes a bug,
or the ``upcoming/v4.X`` branch (if it exists at the moment of submitting the PR)
otherwise.
- We use `Pylint <https://www.pylint.org/>`__ for error and syntax checking of
the source. The file ``.travis.yml`` in the project's root directory
shows how it is invoked. Note that sometimes it might be better to disable a
warning rather than adapting the code to Pylint's desires.
the source and `MyPy <https://github.com/python/mypy>`__ for type checking.
Beware that sometimes it might be better to disable a warning rather than
adapting the code to a tool's desires.
- The documentation source is located in the ``docs`` folder. The file
``cli-options.rst`` is merely an RST-formatted copy of ``instaloader --help``
output, of which the source is in ``instaloader/__main__.py``.
- Feel free to contact us, even if you "only" have Proof-of-Concepts or
not-fully integrated changes. They already might be an advance for the
project.
Proposing Features
------------------
Suggesting Features
-------------------
.. goal-start
If you have an idea of how Instaloader should be enhanced, but do not want to
implement the feature yourself, feel free to open a ticket in our
`Issue Tracker <https://github.com/instaloader/instaloader/issues>`__.
Please consider the following:
Instaloader's goal is to mimic the browser's behavior to access the data that
is available through the Instagram web interface, pack this data into complete
and easily-(re)usable python objects, and provide a user interface for the most
common downloading and metadata collection tasks, without changing any of the
online data.
.. goal-end
Prior spending effort on implementing a new feature, it might be appropriate to
clarify how it could fit into the project's scope or discuss implementation
details. If you feel the need to do so, please create a "feature suggestion".
- Instaloader already has plenty of features. **Check the documentation**
beforehand to ensure your desired suggestion is not already implemented.
@@ -76,13 +82,21 @@ Please consider the following:
an issue suggesting the same or a similar feature, share your thoughts in a
comment there, instead of opening a new issue.
- If possible, provide us a **use case of the feature**: How could the user
invoke the new function? Which problem would it solve? If new information is
obtained, how would it be further processed?
- **Motivate the feature**, i.e.
- **Be patient**. Naturally, bugs and pull requests have a higher priority than
feature suggestions. Keep in mind that this is a free software project, and
unfortunately we only have limited time to work on it.
- Provide us a **use case of the feature**: How could the user
invoke the new function? Which problem would it solve? If new information is
obtained, how would it be further processed?
- Describe already-working **alternatives of the feature** and how they
compare to your proposed feature.
- Briefly describe how your suggested feature **conforms with Instaloader's
project goal**.
- Explain your **solution ideas**. Describe your ideas on how the feature could
be implemented and the underlying problem could be solved. Also **describe
alternatives** that you have considered.
Donations
---------
+10 -2
View File
@@ -15,6 +15,10 @@ Instaloader
**Instaloader** is a tool to download pictures (or videos) along with
their captions and other metadata from Instagram.
.. include:: ../README.rst
:start-after: badges-start
:end-before: badges-end
With `Python <https://www.python.org/>`__ installed, do::
$ pip3 install instaloader
@@ -36,14 +40,17 @@ See :ref:`install` for more options on how to install Instaloader.
- allows **fine-grained customization** of filters and where to store
downloaded media,
- automatically **resumes previously-interrupted** download iterations,
- is free `open source <https://github.com/instaloader/instaloader>`__
software written in Python.
::
instaloader [--comments] [--geotags] [--stories] [--highlights] [--tagged]
instaloader [--comments] [--geotags]
[--stories] [--highlights] [--tagged] [--igtv]
[--login YOUR-USERNAME] [--fast-update]
profile | "#hashtag" |
profile | "#hashtag" | %location_id |
:stories | :feed | :saved
See :ref:`download-pictures-from-instagram` for a detailed introduction on how
@@ -60,6 +67,7 @@ Instaloader Documentation
cli-options
as-module
codesnippets
troubleshooting
contributing
Useful Links
+10 -7
View File
@@ -5,27 +5,27 @@ Install Instaloader
.. highlight:: none
**To install Instaloader**,
To **install Instaloader**,
#. Ensure that you have `Python <https://www.python.org/>`__, at least
version 3.5 and `pip <https://pypi.python.org/pypi/pip>`__
installed
version 3.5, and `pip <https://pypi.python.org/pypi/pip>`__
installed.
#. Then, install Instaloader using::
pip3 install instaloader
**To upgrade Instaloader**, do::
To **upgrade Instaloader** to its current version, do::
pip3 install --upgrade instaloader
**Alternative methods for installing Instaloader:**
**Alternative methods** for installing Instaloader:
- If you do not want to use pip, even though it is highly recommended,
and prefer to **install Instaloader manually**,
`Download the Source <https://github.com/instaloader/instaloader/releases/latest>`__,
extract the Zip or Tarball and execute ``instaloader.py`` from there.
extract the Zip or Tarball and run ``setup.py`` from there.
- On **Arch Linux**, you may install Instaloader using the
`Instaloader AUR package <https://aur.archlinux.org/packages/instaloader/>`__.
@@ -33,6 +33,9 @@ Install Instaloader
- On **Windows 10**, you may download the standalone executable from the
`current release page <https://github.com/instaloader/instaloader/releases/latest>`__.
- To test the most current pre-release or development version of Instaloader::
- On **Android**, you can use Instaloader with `Termux <https://play.google.com/store/apps/details?id=com.termux>`__
after typing ``pkg install python`` and ``pip3 install instaloader``.
- To test the most current **pre-release** version of Instaloader::
pip3 install --pre instaloader
+49
View File
@@ -0,0 +1,49 @@
Exceptions
^^^^^^^^^^
.. module:: instaloader
:noindex:
.. highlight:: python
.. currentmodule:: instaloader.exceptions
.. autoexception:: InstaloaderException
:no-show-inheritance:
.. autoexception:: ConnectionException
.. currentmodule:: instaloader
.. autoexception:: QueryReturnedBadRequestException
.. autoexception:: QueryReturnedForbiddenException
.. autoexception:: ProfileNotExistsException
.. autoexception:: ProfileHasNoPicsException
.. autoexception:: PrivateProfileNotFollowedException
.. autoexception:: LoginRequiredException
.. autoexception:: TwoFactorAuthRequiredException
.. versionadded:: 4.2
.. autoexception:: InvalidArgumentException
.. autoexception:: BadResponseException
.. autoexception:: BadCredentialsException
.. autoexception:: PostChangedException
.. autoexception:: QueryReturnedNotFoundException
.. versionchanged:: 4.3
QueryReturnedNotFoundException now inherits ConnectionException
to retry on 404 errors.
.. autoexception:: TooManyRequestsException
+10
View File
@@ -0,0 +1,10 @@
``Instaloader`` (Main Class)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. module:: instaloader
:noindex:
.. highlight:: python
.. autoclass:: Instaloader
:no-show-inheritance:
+24
View File
@@ -0,0 +1,24 @@
``InstaloaderContext`` (Low-level functions)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. module:: instaloader
:noindex:
.. highlight:: python
.. contents::
:backlinks: none
``InstaloaderContext``
""""""""""""""""""""""
.. autoclass:: InstaloaderContext
:no-show-inheritance:
``RateController``
""""""""""""""""""
.. autoclass:: RateController
:no-show-inheritance:
.. versionadded:: 4.5
+31
View File
@@ -0,0 +1,31 @@
Resumable Iterations
^^^^^^^^^^^^^^^^^^^^
.. module:: instaloader
:noindex:
.. highlight:: python
.. contents::
:backlinks: none
For many download targets, Instaloader is able to resume a
previously-interrupted iteration. It provides an interruptible
Iterator :class:`NodeIterator` and a context manager
:func:`resumable_iteration`, which we both present here.
.. versionadded:: 4.5
``NodeIterator``
""""""""""""""""
.. autoclass:: NodeIterator
:no-show-inheritance:
.. autoclass:: FrozenNodeIterator
:no-show-inheritance:
``resumable_iteration``
"""""""""""""""""""""""
.. autofunction:: resumable_iteration
+82
View File
@@ -0,0 +1,82 @@
Instagram Structures
^^^^^^^^^^^^^^^^^^^^
.. module:: instaloader
:noindex:
.. highlight:: python
.. contents::
:backlinks: none
Posts
"""""
.. autoclass:: Post
:no-show-inheritance:
Additionally, the following trivial structures are defined:
.. autoclass:: PostSidecarNode
:no-show-inheritance:
.. autoclass:: PostComment
:no-show-inheritance:
.. autoclass:: PostCommentAnswer
:no-show-inheritance:
.. autoclass:: PostLocation
:no-show-inheritance:
User Stories
""""""""""""
.. autoclass:: Story
:no-show-inheritance:
.. autoclass:: StoryItem
:no-show-inheritance:
Highlights
""""""""""
.. autoclass:: Highlight
:no-show-inheritance:
:inherited-members:
Bases: :class:`Story`
.. versionadded:: 4.1
Profiles
""""""""
.. autoclass:: Profile
:no-show-inheritance:
Hashtags
""""""""
.. autoclass:: Hashtag
:no-show-inheritance:
.. versionadded:: 4.4
TopSearchResults
""""""""""""""""
.. autoclass:: TopSearchResults
:no-show-inheritance:
.. versionadded:: 4.3
Loading and Saving
""""""""""""""""""
:class:`Post`, :class:`StoryItem`, :class:`Profile`, :class:`Hashtag` and
:class:`FrozenNodeIterator` can be saved and loaded to/from JSON files.
.. autofunction:: load_structure_from_file
.. autofunction:: save_structure_to_file
+1 -2
View File
@@ -1,3 +1,2 @@
requests
sphinx!=1.7.7
sphinx-bootstrap-theme
sphinx~=2.3
+36 -13
View File
@@ -1,4 +1,4 @@
# sphinx_autodoc_typehints from Mar 18 2018.
# sphinx_autodoc_typehints from Nov 20 2018 (version 1.5.2).
# https://github.com/agronholm/sphinx-autodoc-typehints
# (slightly modified to fix class links, maybe related to agronholm/sphinx-autodoc-typehints#38)
#
@@ -27,8 +27,8 @@
import inspect
from typing import get_type_hints, TypeVar, Any, AnyStr, Generic, Union
from sphinx.util.inspect import getargspec
from sphinx.ext.autodoc import formatargspec
from sphinx.util import logging
from sphinx.util.inspect import Signature
try:
from inspect import unwrap
@@ -51,6 +51,8 @@ except ImportError:
memo.add(id_func)
return func
logger = logging.getLogger(__name__)
def format_annotation(annotation):
if inspect.isclass(annotation) and annotation.__module__ == 'builtins':
@@ -162,23 +164,37 @@ def process_signature(app, what: str, name: str, obj, options, signature, return
return
obj = unwrap(obj)
try:
argspec = getargspec(obj)
except (TypeError, ValueError):
return
signature = Signature(obj)
parameters = [
param.replace(annotation=inspect.Parameter.empty)
for param in signature.signature.parameters.values()
]
if argspec.args:
if parameters:
if what in ('class', 'exception'):
del argspec.args[0]
del parameters[0]
elif what == 'method':
outer = inspect.getmodule(obj)
for clsname in obj.__qualname__.split('.')[:-1]:
outer = getattr(outer, clsname)
method_object = outer.__dict__[obj.__name__]
if not isinstance(method_object, (classmethod, staticmethod)):
del argspec.args[0]
return formatargspec(obj, *argspec[:-1]), None
method_name = obj.__name__
if method_name.startswith("__") and not method_name.endswith("__"):
# If the method starts with double underscore (dunder)
# Python applies mangling so we need to prepend the class name.
# This doesn't happen if it always ends with double underscore.
class_name = obj.__qualname__.split('.')[-2]
method_name = "_{c}{m}".format(c=class_name, m=method_name)
method_object = outer.__dict__[method_name]
if not isinstance(method_object, (classmethod, staticmethod)):
del parameters[0]
signature.signature = signature.signature.replace(
parameters=parameters,
return_annotation=inspect.Signature.empty)
return signature.format_args().replace('\\', '\\\\'), None
def process_docstring(app, what, name, obj, options, lines):
@@ -195,8 +211,15 @@ def process_docstring(app, what, name, obj, options, lines):
except (AttributeError, TypeError):
# Introspecting a slot wrapper will raise TypeError
return
except NameError as exc:
logger.warning('Cannot resolve forward reference in type annotations of "%s": %s',
name, exc)
type_hints = obj.__annotations__
for argname, annotation in type_hints.items():
if argname.endswith('_'):
argname = '{}\\_'.format(argname[:-1])
formatted_annotation = format_annotation(annotation)
if argname == 'return':
+91
View File
@@ -0,0 +1,91 @@
.. _troubleshooting:
Troubleshooting
===============
.. highlight:: python
429 Too Many Requests
---------------------
Instaloader has a logic to keep track of its requests to Instagram and to obey
their rate limits. The rate controller assumes that
- at one time, Instaloader is the only application that consumes requests, i.e.
neither the Instagram browser interface, nor a mobile app, nor another
Instaloader instance is running in parallel, and
- no requests had been consumed when Instaloader starts.
The latter one implies that restarting or reinstantiating Instaloader often
within short time is prone to cause a 429.
Since the behavior of the rate controller might change between different
versions of Instaloader, make sure to use the current version of Instaloader,
especially when encountering many 429 errors.
If a request is denied with a 429,
Instaloader retries the request as soon as the temporary ban is assumed to be
expired. In case the retry continuously fails for some reason, which should not
happen under normal conditions, consider adjusting the
:option:`--max-connection-attempts` option.
There have been observations that services, that in their nature offer
promiscuous IP addresses, such as cloud, VPN and public proxy services, might be
subject to significantly stricter limits for anonymous access. However,
logged-in accesses (see :option:`--login`) do not seem to be affected.
Instaloader allows to adjust the rate controlling behavior by overriding
:class:`instaloader.RateController`.
Too many queries in the last time
---------------------------------
**"Too many queries in the last time"** is not an error. It is a notice that the
rate limit has almost been reached, according to Instaloader's own rate
accounting mechanism.
Instaloader allows to adjust the rate controlling behavior by overriding
:class:`instaloader.RateController`.
Private but not followed
------------------------
You have to follow a private account to access most of its associated
information.
Login error
-----------
Instaloader's login *should* work fine, both with and without
Two-Factor-Authentication. It also supports handling the *checkpoint challenge*,
issued when Instagram suspects authentication activity on your account, by
pointing the user to an URL to be opened in a browser.
Nevertheless, in :issue:`92` and :issue:`615` users reported problems with
logging in. We recommend to always keep the session file which Instaloader
creates when using :option:`--login`. If a session file is present,
:option:`--login` does not make use of the failure-prone login procedure.
Also, session files usually do not expire.
If you do not have a session file present, you may use the following script
(:example:`615_import_firefox_session.py`) to workaround login problems by
importing the session cookies from Firefox and bypassing Instaloader's login and
so still use Instaloader's logged-in functionality.
.. literalinclude:: codesnippets/615_import_firefox_session.py
To use this script,
#. Download the script: :example:`615_import_firefox_session.py`,
#. Login to Instagram in Firefox,
#. Execute the snippet, e.g. with ``python 615_import_firefox_session.py``,
#. Then, ``instaloader -l USERNAME`` should work fine.
This script also supports specifying a cookie file path, which may be useful if
you use multiple Firefox profiles or if your operating system has the directory
structure differently set up. Also, you can specify an alternative session file
path.
+6 -5
View File
@@ -1,12 +1,12 @@
"""Download pictures (or videos) along with their captions and other metadata from Instagram."""
__version__ = '4.1'
__version__ = '4.5.4'
try:
# pylint:disable=wrong-import-position
import win_unicode_console
import win_unicode_console # type: ignore
except ImportError:
pass
else:
@@ -14,6 +14,7 @@ else:
from .exceptions import *
from .instaloader import Instaloader
from .instaloadercontext import InstaloaderContext
from .structures import (Highlight, Post, PostSidecarNode, PostComment, PostLocation, Profile, Story, StoryItem,
load_structure_from_file, save_structure_to_file)
from .instaloadercontext import InstaloaderContext, RateController
from .nodeiterator import NodeIterator, FrozenNodeIterator, resumable_iteration
from .structures import (Hashtag, Highlight, Post, PostSidecarNode, PostComment, PostCommentAnswer, PostLocation,
Profile, Story, StoryItem, TopSearchResults, load_structure_from_file, save_structure_to_file)
+74 -19
View File
@@ -3,12 +3,14 @@
import ast
import datetime
import os
import re
import sys
from argparse import ArgumentParser, SUPPRESS
from typing import List, Optional
from . import (Instaloader, InstaloaderException, InvalidArgumentException, Post, Profile, ProfileNotExistsException,
StoryItem, __version__, load_structure_from_file)
StoryItem, __version__, load_structure_from_file, TwoFactorAuthRequiredException,
BadCredentialsException)
from .instaloader import get_default_session_filename
from .instaloadercontext import default_user_agent
@@ -18,9 +20,10 @@ def usage_string():
argv0 = os.path.basename(sys.argv[0])
argv0 = "instaloader" if argv0 == "__main__.py" else argv0
return """
{0} [--comments] [--geotags] [--stories] [--highlights] [--tagged]
{0} [--comments] [--geotags]
{2:{1}} [--stories] [--highlights] [--tagged] [--igtv]
{2:{1}} [--login YOUR-USERNAME] [--fast-update]
{2:{1}} profile | "#hashtag" | :stories | :feed | :saved
{2:{1}} profile | "#hashtag" | %%location_id | :stories | :feed | :saved
{0} --help""".format(argv0, len(argv0), '')
@@ -60,7 +63,10 @@ def _main(instaloader: Instaloader, targetlist: List[str],
username: Optional[str] = None, password: Optional[str] = None,
sessionfile: Optional[str] = None,
download_profile_pic: bool = True, download_posts=True,
download_stories: bool = False, download_highlights: bool = False, download_tagged: bool = False,
download_stories: bool = False,
download_highlights: bool = False,
download_tagged: bool = False,
download_igtv: bool = False,
fast_update: bool = False,
max_count: Optional[int] = None, post_filter_str: Optional[str] = None,
storyitem_filter_str: Optional[str] = None) -> None:
@@ -76,6 +82,8 @@ def _main(instaloader: Instaloader, targetlist: List[str],
instaloader.context.log('Only download storyitems with property "{}".'.format(storyitem_filter_str))
# Login, if desired
if username is not None:
if not re.match(r"^[A-Za-z0-9._]+$", username):
instaloader.context.error("Warning: Parameter \"{}\" for --login is not a valid username.".format(username))
try:
instaloader.load_session_from_file(username, sessionfile)
except FileNotFoundError as err:
@@ -84,10 +92,22 @@ def _main(instaloader: Instaloader, targetlist: List[str],
instaloader.context.log("Session file does not exist yet - Logging in.")
if not instaloader.context.is_logged_in or username != instaloader.test_login():
if password is not None:
instaloader.login(username, password)
try:
instaloader.login(username, password)
except TwoFactorAuthRequiredException:
while True:
try:
code = input("Enter 2FA verification code: ")
instaloader.two_factor_login(code)
break
except BadCredentialsException:
pass
else:
instaloader.interactive_login(username)
instaloader.context.log("Logged in as %s." % username)
# since 4.2.9 login is required for geotags
if instaloader.download_geotags and not instaloader.context.is_logged_in:
instaloader.context.error("Warning: Use --login to download geotags of posts.")
# Try block for KeyboardInterrupt (save session on ^C)
profiles = set()
anonymous_retry_profiles = set()
@@ -119,17 +139,21 @@ def _main(instaloader: Instaloader, targetlist: List[str],
# strip '/' characters to be more shell-autocompletion-friendly
target = target.rstrip('/')
with instaloader.context.error_catcher(target):
if target[0] == '@':
if re.match(r"^@[A-Za-z0-9._]+$", target):
instaloader.context.log("Retrieving followees of %s..." % target[1:])
profile = Profile.from_username(instaloader.context, target[1:])
for followee in profile.get_followees():
instaloader.save_profile_id(followee)
profiles.add(followee)
elif target[0] == '#':
elif re.match(r"^#\w+$", target):
instaloader.download_hashtag(hashtag=target[1:], max_count=max_count, fast_update=fast_update,
post_filter=post_filter)
elif target[0] == '-':
post_filter=post_filter,
profile_pic=download_profile_pic, posts=download_posts)
elif re.match(r"^-[A-Za-z0-9-_]+$", target):
instaloader.download_post(Post.from_shortcode(instaloader.context, target[1:]), target)
elif re.match(r"^%[0-9]+$", target):
instaloader.download_location(location=target[1:], max_count=max_count, fast_update=fast_update,
post_filter=post_filter)
elif target == ":feed":
instaloader.download_feed_posts(fast_update=fast_update, max_count=max_count,
post_filter=post_filter)
@@ -138,11 +162,12 @@ def _main(instaloader: Instaloader, targetlist: List[str],
elif target == ":saved":
instaloader.download_saved_posts(fast_update=fast_update, max_count=max_count,
post_filter=post_filter)
else:
elif re.match(r"^[A-Za-z0-9._]+$", target):
try:
profile = instaloader.check_profile_id(target)
if instaloader.context.is_logged_in and profile.has_blocked_viewer:
if download_profile_pic or ((download_posts or download_tagged) and not profile.is_private):
if download_profile_pic or ((download_posts or download_tagged or download_igtv)
and not profile.is_private):
raise ProfileNotExistsException("{} blocked you; But we download her anonymously."
.format(target))
else:
@@ -153,7 +178,7 @@ def _main(instaloader: Instaloader, targetlist: List[str],
# Not only our profile.has_blocked_viewer condition raises ProfileNotExistsException,
# check_profile_id() also does, since access to blocked profile may be responded with 404.
if instaloader.context.is_logged_in and (download_profile_pic or download_posts or
download_tagged):
download_tagged or download_igtv):
instaloader.context.log(err)
instaloader.context.log("Trying again anonymously, helps in case you are just blocked.")
with instaloader.anonymous_copy() as anonymous_loader:
@@ -163,18 +188,28 @@ def _main(instaloader: Instaloader, targetlist: List[str],
.format(target, err))
else:
raise
else:
target_type = {
'#': 'hashtag',
'%': 'location',
'-': 'shortcode',
}.get(target[0], 'username')
raise ProfileNotExistsException('Invalid {} {}'.format(target_type, target))
if len(profiles) > 1:
instaloader.context.log("Downloading {} profiles: {}".format(len(profiles),
' '.join([p.username for p in profiles])))
if profiles and (download_profile_pic or download_posts) and not instaloader.context.is_logged_in:
instaloader.context.error("Warning: Use --login to download higher-quality versions of pictures.")
instaloader.download_profiles(profiles,
download_profile_pic, download_posts, download_tagged, download_highlights,
download_stories, fast_update, post_filter, storyitem_filter)
download_profile_pic, download_posts, download_tagged, download_igtv,
download_highlights, download_stories,
fast_update, post_filter, storyitem_filter)
if anonymous_retry_profiles:
instaloader.context.log("Downloading anonymously: {}"
.format(' '.join([p.username for p in anonymous_retry_profiles])))
with instaloader.anonymous_copy() as anonymous_loader:
anonymous_loader.download_profiles(anonymous_retry_profiles,
download_profile_pic, download_posts, download_tagged,
download_profile_pic, download_posts, download_tagged, download_igtv,
fast_update=fast_update, post_filter=post_filter)
except KeyboardInterrupt:
print("\nInterrupted by user.", file=sys.stderr)
@@ -208,6 +243,8 @@ def main():
help="Download all followees of profile. Requires --login. "
"Consider using :feed rather than @yourself.")
g_targets.add_argument('_hashtag', nargs='*', metavar='"#hashtag"', help="Download #hashtag.")
g_targets.add_argument('_location', nargs='*', metavar='%location_id',
help="Download %%location_id. Requires --login.")
g_targets.add_argument('_feed', nargs='*', metavar=":feed",
help="Download pictures from your feed. Requires --login.")
g_targets.add_argument('_stories', nargs='*', metavar=":stories",
@@ -242,7 +279,7 @@ def main():
help='Download geotags when available. Geotags are stored as a '
'text file with the location\'s name and a Google Maps link. '
'This requires an additional request to the Instagram '
'server for each picture, which is why it is disabled by default.')
'server for each picture. Requires --login.')
g_post.add_argument('-C', '--comments', action='store_true',
help='Download and update comments for each post. '
'This requires an additional request to the Instagram '
@@ -267,6 +304,8 @@ def main():
help='Also download highlights of each profile that is downloaded. Requires --login.')
g_prof.add_argument('--tagged', action='store_true',
help='Also download posts where each profile is tagged.')
g_prof.add_argument('--igtv', action='store_true',
help='Also download IGTV videos.')
g_cond = parser.add_argument_group("Which Posts to Download")
@@ -312,14 +351,23 @@ def main():
'--dirname-pattern. {profile} is replaced by the profile name,'
'{target} is replaced by the target you specified, i.e. either :feed'
'#hashtag or the profile name. Defaults to \'{date_utc}_UTC\'')
g_how.add_argument('--resume-prefix', metavar='PREFIX',
help='Prefix for filenames that are used to save the information to resume an interrupted '
'download.')
g_how.add_argument('--no-resume', action='store_true',
help='Do not resume a previously-aborted download iteration, and do not save such information '
'when interrupted.')
g_how.add_argument('--use-aged-resume-files', action='store_true', help=SUPPRESS)
g_how.add_argument('--user-agent',
help='User Agent to use for HTTP requests. Defaults to \'{}\'.'.format(default_user_agent()))
g_how.add_argument('-S', '--no-sleep', action='store_true', help=SUPPRESS)
g_how.add_argument('--graphql-rate-limit', type=int, help=SUPPRESS)
g_how.add_argument('--max-connection-attempts', metavar='N', type=int, default=3,
help='Maximum number of connection attempts until a request is aborted. Defaults to 3. If a '
'connection fails, it can be manually skipped by hitting CTRL+C. Set this to 0 to retry '
'infinitely.')
g_how.add_argument('--commit-mode', action='store_true', help=SUPPRESS)
g_how.add_argument('--request-timeout', metavar='N', type=float,
help='seconds to wait before timing out a connection request')
g_misc = parser.add_argument_group('Miscellaneous Options')
g_misc.add_argument('-q', '--quiet', action='store_true',
@@ -353,6 +401,10 @@ def main():
raise SystemExit("--no-captions and --post-metadata-txt or --storyitem-metadata-txt given; "
"That contradicts.")
if args.no_resume and args.resume_prefix:
raise SystemExit("--no-resume and --resume-prefix given; That contradicts.")
resume_prefix = (args.resume_prefix if args.resume_prefix else 'iterator') if not args.no_resume else None
if args.no_pictures and args.fast_update:
raise SystemExit('--no-pictures and --fast-update cannot be used together.')
@@ -370,8 +422,10 @@ def main():
compress_json=not args.no_compress_json,
post_metadata_txt_pattern=post_metadata_txt_pattern,
storyitem_metadata_txt_pattern=storyitem_metadata_txt_pattern,
graphql_rate_limit=args.graphql_rate_limit,
max_connection_attempts=args.max_connection_attempts)
max_connection_attempts=args.max_connection_attempts,
request_timeout=args.request_timeout,
resume_prefix=resume_prefix,
check_resume_bbd=not args.use_aged_resume_files)
_main(loader,
args.profile,
username=args.login.lower() if args.login is not None else None,
@@ -382,6 +436,7 @@ def main():
download_stories=download_stories,
download_highlights=args.highlights,
download_tagged=args.tagged,
download_igtv=args.igtv,
fast_update=args.fast_update,
max_count=int(args.count) if args.count is not None else None,
post_filter_str=args.post_filter,
+17 -4
View File
@@ -9,10 +9,6 @@ class QueryReturnedBadRequestException(InstaloaderException):
pass
class QueryReturnedNotFoundException(InstaloaderException):
pass
class QueryReturnedForbiddenException(InstaloaderException):
pass
@@ -22,6 +18,10 @@ class ProfileNotExistsException(InstaloaderException):
class ProfileHasNoPicsException(InstaloaderException):
"""
.. deprecated:: 4.2.2
Not raised anymore.
"""
pass
@@ -33,6 +33,10 @@ class LoginRequiredException(InstaloaderException):
pass
class TwoFactorAuthRequiredException(InstaloaderException):
pass
class InvalidArgumentException(InstaloaderException):
pass
@@ -49,5 +53,14 @@ class ConnectionException(InstaloaderException):
pass
class PostChangedException(InstaloaderException):
""".. versionadded:: 4.2.2"""
pass
class QueryReturnedNotFoundException(ConnectionException):
pass
class TooManyRequestsException(ConnectionException):
pass
+524 -224
View File
File diff suppressed because it is too large Load Diff
+321 -89
View File
@@ -1,5 +1,6 @@
import hashlib
import json
import os
import pickle
import random
import re
@@ -10,20 +11,24 @@ import time
import urllib.parse
from contextlib import contextmanager
from datetime import datetime, timedelta
from typing import Any, Callable, Dict, Iterator, Optional
from functools import partial
from typing import Any, Callable, Dict, Iterator, List, Optional, Union
import requests
import requests.utils
import urllib3
from .exceptions import *
def copy_session(session: requests.Session) -> requests.Session:
def copy_session(session: requests.Session, request_timeout: Optional[float] = None) -> requests.Session:
"""Duplicates a requests.Session."""
new = requests.Session()
new.cookies = requests.utils.cookiejar_from_dict(requests.utils.dict_from_cookiejar(session.cookies))
new.headers = session.headers.copy()
if request_timeout is not None:
# Override default timeout behavior.
# Need to silence mypy bug for this. See: https://github.com/python/mypy/issues/2427
new.request = partial(new.request, timeout=request_timeout) # type: ignore
return new
@@ -48,29 +53,30 @@ class InstaloaderContext:
"""
def __init__(self, sleep: bool = True, quiet: bool = False, user_agent: Optional[str] = None,
graphql_count_per_slidingwindow: Optional[int] = None, max_connection_attempts: int = 3):
max_connection_attempts: int = 3, request_timeout: Optional[float] = None,
rate_controller: Optional[Callable[["InstaloaderContext"], "RateController"]] = None):
self.user_agent = user_agent if user_agent is not None else default_user_agent()
self.request_timeout = request_timeout
self._session = self.get_anonymous_session()
self.username = None
self.sleep = sleep
self.quiet = quiet
self.max_connection_attempts = max_connection_attempts
self._graphql_page_length = 50
self.graphql_count_per_slidingwindow = graphql_count_per_slidingwindow or 200
self._root_rhx_gis = None
self.two_factor_auth_pending = None
# error log, filled with error() and printed at the end of Instaloader.main()
self.error_log = []
self.error_log = [] # type: List[str]
# For the adaption of sleep intervals (rate control)
self.query_timestamps = list()
self._rate_controller = rate_controller(self) if rate_controller is not None else RateController(self)
# Can be set to True for testing, disables supression of InstaloaderContext._error_catcher
self.raise_all_errors = False
# Cache profile from id (mapping from id to Profile)
self.profile_id_cache = dict()
self.profile_id_cache = dict() # type: Dict[int, Any]
@contextmanager
def anonymous_copy(self):
@@ -107,7 +113,7 @@ class InstaloaderContext:
def close(self):
"""Print error log and close session"""
if self.error_log and not self.quiet:
print("\nErrors occured:", file=sys.stderr)
print("\nErrors or warnings occurred:", file=sys.stderr)
for err in self.error_log:
print(err, file=sys.stderr)
self._session.close()
@@ -155,6 +161,10 @@ class InstaloaderContext:
'ig_vw': '1920', 'csrftoken': '',
's_network': '', 'ds_user_id': ''})
session.headers.update(self._default_http_header(empty_session_only=True))
if self.request_timeout is not None:
# Override default timeout behavior.
# Need to silence mypy bug for this. See: https://github.com/python/mypy/issues/2427
session.request = partial(session.request, timeout=self.request_timeout) # type: ignore
return session
def save_session_to_file(self, sessionfile):
@@ -167,6 +177,10 @@ class InstaloaderContext:
session.cookies = requests.utils.cookiejar_from_dict(pickle.load(sessionfile))
session.headers.update(self._default_http_header())
session.headers.update({'X-CSRFToken': session.cookies.get_dict()['csrftoken']})
if self.request_timeout is not None:
# Override default timeout behavior.
# Need to silence mypy bug for this. See: https://github.com/python/mypy/issues/2427
session.request = partial(session.request, timeout=self.request_timeout) # type: ignore
self._session = session
self.username = username
@@ -180,29 +194,60 @@ class InstaloaderContext:
:raises InvalidArgumentException: If the provided username does not exist.
:raises BadCredentialsException: If the provided password is wrong.
:raises ConnectionException: If connection to Instagram failed."""
:raises ConnectionException: If connection to Instagram failed.
:raises TwoFactorAuthRequiredException: First step of 2FA login done, now call
:meth:`Instaloader.two_factor_login`."""
# pylint:disable=import-outside-toplevel
import http.client
# pylint:disable=protected-access
http.client._MAXHEADERS = 200
session = requests.Session()
session.cookies.update({'sessionid': '', 'mid': '', 'ig_pr': '1',
'ig_vw': '1920', 'csrftoken': '',
'ig_vw': '1920', 'ig_cb': '1', 'csrftoken': '',
's_network': '', 'ds_user_id': ''})
session.headers.update(self._default_http_header())
session.headers.update({'X-CSRFToken': self.get_json('', {})['config']['csrf_token']})
if self.request_timeout is not None:
# Override default timeout behavior.
# Need to silence mypy bug for this. See: https://github.com/python/mypy/issues/2427
session.request = partial(session.request, timeout=self.request_timeout) # type: ignore
session.get('https://www.instagram.com/web/__mid/')
csrf_token = session.cookies.get_dict()['csrftoken']
session.headers.update({'X-CSRFToken': csrf_token})
# Not using self.get_json() here, because we need to access csrftoken cookie
self._sleep()
self.do_sleep()
# Workaround credits to pgrimaud.
# See: https://github.com/pgrimaud/instagram-user-feed/commit/96ad4cf54d1ad331b337f325c73e664999a6d066
enc_password = '#PWD_INSTAGRAM_BROWSER:0:{}:{}'.format(int(datetime.now().timestamp()), passwd)
login = session.post('https://www.instagram.com/accounts/login/ajax/',
data={'password': passwd, 'username': user}, allow_redirects=True)
if login.status_code != 200:
raise ConnectionException("Login error: {} {}".format(login.status_code, login.reason))
resp_json = login.json()
data={'enc_password': enc_password, 'username': user}, allow_redirects=True)
try:
resp_json = login.json()
except json.decoder.JSONDecodeError:
raise ConnectionException("Login error: JSON decode fail, {} - {}.".format(login.status_code, login.reason))
if resp_json.get('two_factor_required'):
two_factor_session = copy_session(session, self.request_timeout)
two_factor_session.headers.update({'X-CSRFToken': csrf_token})
two_factor_session.cookies.update({'csrftoken': csrf_token})
self.two_factor_auth_pending = (two_factor_session,
user,
resp_json['two_factor_info']['two_factor_identifier'])
raise TwoFactorAuthRequiredException("Login error: two-factor authentication required.")
if resp_json.get('checkpoint_url'):
raise ConnectionException("Login: Checkpoint required. Point your browser to "
"https://www.instagram.com{} - "
"follow the instructions, then retry.".format(resp_json.get('checkpoint_url')))
if resp_json['status'] != 'ok':
if 'message' in resp_json:
raise ConnectionException("Login error: \"{}\" status, message \"{}\".".format(resp_json['status'],
resp_json['message']))
else:
raise ConnectionException("Login error: \"{}\" status.".format(resp_json['status']))
if 'authenticated' not in resp_json:
# Issue #472
if 'message' in resp_json:
raise ConnectionException("Login error: Unexpected response, \"{}\".".format(resp_json['message']))
else:
raise ConnectionException("Login error: Unexpected response, this might indicate a blocked IP.")
if not resp_json['authenticated']:
if resp_json['user']:
# '{"authenticated": false, "user": true, "status": "ok"}'
@@ -218,7 +263,33 @@ class InstaloaderContext:
self._session = session
self.username = user
def _sleep(self):
def two_factor_login(self, two_factor_code):
"""Second step of login if 2FA is enabled.
Not meant to be used directly, use :meth:`Instaloader.two_factor_login`.
:raises InvalidArgumentException: No two-factor authentication pending.
:raises BadCredentialsException: 2FA verification code invalid.
.. versionadded:: 4.2"""
if not self.two_factor_auth_pending:
raise InvalidArgumentException("No two-factor authentication pending.")
(session, user, two_factor_id) = self.two_factor_auth_pending
login = session.post('https://www.instagram.com/accounts/login/ajax/two_factor/',
data={'username': user, 'verificationCode': two_factor_code, 'identifier': two_factor_id},
allow_redirects=True)
resp_json = login.json()
if resp_json['status'] != 'ok':
if 'message' in resp_json:
raise BadCredentialsException("Login error: {}".format(resp_json['message']))
else:
raise BadCredentialsException("Login error: \"{}\" status.".format(resp_json['status']))
session.headers.update({'X-CSRFToken': login.cookies['csrftoken']})
self._session = session
self.username = user
self.two_factor_auth_pending = None
def do_sleep(self):
"""Sleep a short time if self.sleep is set. Called before each request to instagram.com."""
if self.sleep:
time.sleep(min(random.expovariate(0.7), 5.0))
@@ -236,36 +307,25 @@ class InstaloaderContext:
:raises QueryReturnedNotFoundException: When the server responds with a 404.
:raises ConnectionException: When query repeatedly failed.
"""
def graphql_query_waittime(untracked_queries: bool = False) -> int:
sliding_window = 660
if not self.query_timestamps:
return sliding_window if untracked_queries else 0
current_time = time.monotonic()
self.query_timestamps = list(filter(lambda t: t > current_time - sliding_window, self.query_timestamps))
if len(self.query_timestamps) < self.graphql_count_per_slidingwindow and not untracked_queries:
return 0
return round(min(self.query_timestamps) + sliding_window - current_time) + 6
is_graphql_query = 'query_hash' in params and 'graphql/query' in path
# some queries are not rate limited if invoked anonymously:
query_not_limited = is_graphql_query and not self.is_logged_in \
and params['query_hash'] in ['9ca88e465c3f866a76f7adee3871bdd8']
if is_graphql_query and not query_not_limited:
waittime = graphql_query_waittime()
if waittime > 0:
self.log('\nToo many queries in the last time. Need to wait {} seconds, until {:%H:%M}.'
.format(waittime, datetime.now() + timedelta(seconds=waittime)))
time.sleep(waittime)
if self.query_timestamps is not None:
self.query_timestamps.append(time.monotonic())
else:
self.query_timestamps = [time.monotonic()]
is_iphone_query = host == 'i.instagram.com'
is_other_query = not is_graphql_query and host == "www.instagram.com"
sess = session if session else self._session
try:
self._sleep()
self.do_sleep()
if is_graphql_query:
self._rate_controller.wait_before_query(params['query_hash'])
if is_iphone_query:
self._rate_controller.wait_before_query('iphone')
if is_other_query:
self._rate_controller.wait_before_query('other')
resp = sess.get('https://{0}/{1}'.format(host, path), params=params, allow_redirects=False)
while resp.is_redirect:
redirect_url = resp.headers['location']
self.log('\nHTTP redirect from https://{0}/{1} to {2}'.format(host, path, redirect_url))
if redirect_url.startswith('https://www.instagram.com/accounts/login'):
# alternate rate limit exceeded behavior
raise TooManyRequestsException("429 Too Many Requests: redirected to login")
if redirect_url.startswith('https://{}/'.format(host)):
resp = sess.get(redirect_url if redirect_url.endswith('/') else redirect_url + '/',
params=params, allow_redirects=False)
@@ -283,8 +343,19 @@ class InstaloaderContext:
if is_html_query:
match = re.search(r'window\._sharedData = (.*);</script>', resp.text)
if match is None:
raise ConnectionException("Could not find \"window._sharedData\" in html response.")
return json.loads(match.group(1))
raise QueryReturnedNotFoundException("Could not find \"window._sharedData\" in html response.")
resp_json = json.loads(match.group(1))
entry_data = resp_json.get('entry_data')
post_or_profile_page = list(entry_data.values())[0] if entry_data is not None else None
if post_or_profile_page is None:
raise ConnectionException("\"window._sharedData\" does not contain required keys.")
# If GraphQL data is missing in `window._sharedData`, search for it in `__additionalDataLoaded`.
if 'graphql' not in post_or_profile_page[0]:
match = re.search(r'window\.__additionalDataLoaded\(.*?({.*"graphql":.*})\);</script>',
resp.text)
if match is not None:
post_or_profile_page[0]['graphql'] = json.loads(match.group(1))['graphql']
return resp_json
else:
resp_json = resp.json()
if 'status' in resp_json and resp_json['status'] != "ok":
@@ -297,21 +368,19 @@ class InstaloaderContext:
except (ConnectionException, json.decoder.JSONDecodeError, requests.exceptions.RequestException) as err:
error_string = "JSON Query to {}: {}".format(path, err)
if _attempt == self.max_connection_attempts:
raise ConnectionException(error_string) from err
if isinstance(err, QueryReturnedNotFoundException):
raise QueryReturnedNotFoundException(error_string) from err
else:
raise ConnectionException(error_string) from err
self.error(error_string + " [retrying; skip with ^C]", repeat_at_end=False)
text_for_429 = ("HTTP error code 429 was returned because too many queries occured in the last time. "
"Please do not use Instagram in your browser or run multiple instances of Instaloader "
"in parallel.")
try:
if isinstance(err, TooManyRequestsException):
print(textwrap.fill(text_for_429), file=sys.stderr)
if is_graphql_query:
waittime = graphql_query_waittime(untracked_queries=True)
if waittime > 0:
self.log('The request will be retried in {} seconds, at {:%H:%M}.'
.format(waittime, datetime.now() + timedelta(seconds=waittime)))
time.sleep(waittime)
self._sleep()
self._rate_controller.handle_429(params['query_hash'])
if is_iphone_query:
self._rate_controller.handle_429('iphone')
if is_other_query:
self._rate_controller.handle_429('other')
return self.get_json(path=path, params=params, host=host, session=sess, _attempt=_attempt + 1)
except KeyboardInterrupt:
self.error("[skipped by user]", repeat_at_end=False)
@@ -328,7 +397,7 @@ class InstaloaderContext:
:param rhx_gis: 'rhx_gis' variable as somewhere returned by Instagram, needed to 'sign' request
:return: The server's response dictionary.
"""
with copy_session(self._session) as tmpsession:
with copy_session(self._session, self.request_timeout) as tmpsession:
tmpsession.headers.update(self._default_http_header(empty_session_only=True))
del tmpsession.headers['Connection']
del tmpsession.headers['Content-Length']
@@ -359,7 +428,12 @@ class InstaloaderContext:
edge_extractor: Callable[[Dict[str, Any]], Dict[str, Any]],
rhx_gis: Optional[str] = None,
first_data: Optional[Dict[str, Any]] = None) -> Iterator[Dict[str, Any]]:
"""Retrieve a list of GraphQL nodes."""
"""
Retrieve a list of GraphQL nodes.
.. deprecated:: 4.5
Use :class:`NodeIterator` instead, which provides more functionality.
"""
def _query():
query_variables['first'] = self._graphql_page_length
@@ -385,39 +459,65 @@ class InstaloaderContext:
data = _query()
yield from (edge['node'] for edge in data['edges'])
def get_and_write_raw(self, url: str, filename: str, _attempt=1) -> None:
"""Downloads raw data.
def get_iphone_json(self, path: str, params: Dict[str, Any]) -> Dict[str, Any]:
"""JSON request to ``i.instagram.com``.
:param path: URL, relative to ``i.instagram.com/``
:param params: GET parameters
:return: Decoded response dictionary
:raises QueryReturnedBadRequestException: When the server responds with a 400.
:raises QueryReturnedNotFoundException: When the server responds with a 404.
:raises ConnectionException: When query repeatedly failed.
.. versionadded:: 4.2.1"""
with copy_session(self._session, self.request_timeout) as tempsession:
tempsession.headers['User-Agent'] = 'Instagram 123.1.0.26.115 (iPhone12,1; iOS 13_3; en_US; en-US; ' \
'scale=2.00; 1656x3584; 190542906)'
for header in ['Host', 'Origin', 'X-Instagram-AJAX', 'X-Requested-With']:
tempsession.headers.pop(header, None)
return self.get_json(path, params, 'i.instagram.com', tempsession)
def write_raw(self, resp: Union[bytes, requests.Response], filename: str) -> None:
"""Write raw response data into a file.
.. versionadded:: 4.2.1"""
self.log(filename, end=' ', flush=True)
with open(filename + '.temp', 'wb') as file:
if isinstance(resp, requests.Response):
shutil.copyfileobj(resp.raw, file)
else:
file.write(resp)
os.replace(filename + '.temp', filename)
def get_raw(self, url: str, _attempt=1) -> requests.Response:
"""Downloads a file anonymously.
:raises QueryReturnedNotFoundException: When the server responds with a 404.
:raises QueryReturnedForbiddenException: When the server responds with a 403.
:raises ConnectionException: When download failed.
.. versionadded:: 4.2.1"""
with self.get_anonymous_session() as anonymous_session:
resp = anonymous_session.get(url, stream=True)
if resp.status_code == 200:
resp.raw.decode_content = True
return resp
else:
if resp.status_code == 403:
# suspected invalid URL signature
raise QueryReturnedForbiddenException("403 when accessing {}.".format(url))
if resp.status_code == 404:
# 404 not worth retrying.
raise QueryReturnedNotFoundException("404 when accessing {}.".format(url))
raise ConnectionException("HTTP error code {}.".format(resp.status_code))
def get_and_write_raw(self, url: str, filename: str) -> None:
"""Downloads and writes anonymously-requested raw data into a file.
:raises QueryReturnedNotFoundException: When the server responds with a 404.
:raises QueryReturnedForbiddenException: When the server responds with a 403.
:raises ConnectionException: When download repeatedly failed."""
try:
with self.get_anonymous_session() as anonymous_session:
resp = anonymous_session.get(url, stream=True)
if resp.status_code == 200:
self.log(filename, end=' ', flush=True)
with open(filename, 'wb') as file:
resp.raw.decode_content = True
shutil.copyfileobj(resp.raw, file)
else:
if resp.status_code == 403:
# suspected invalid URL signature
raise QueryReturnedForbiddenException("403 when accessing {}.".format(url))
if resp.status_code == 404:
# 404 not worth retrying.
raise QueryReturnedNotFoundException("404 when accessing {}.".format(url))
raise ConnectionException("HTTP error code {}.".format(resp.status_code))
except (urllib3.exceptions.HTTPError, requests.exceptions.RequestException, ConnectionException) as err:
error_string = "URL {}: {}".format(url, err)
if _attempt == self.max_connection_attempts:
raise ConnectionException(error_string) from err
self.error(error_string + " [retrying; skip with ^C]", repeat_at_end=False)
try:
self._sleep()
self.get_and_write_raw(url, filename, _attempt + 1)
except KeyboardInterrupt:
self.error("[skipped by user]", repeat_at_end=False)
raise ConnectionException(error_string) from err
self.write_raw(self.get_raw(url), filename)
@property
def root_rhx_gis(self) -> Optional[str]:
@@ -426,6 +526,138 @@ class InstaloaderContext:
# 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
if self._root_rhx_gis is None:
self._root_rhx_gis = self.get_json('', {}).get('rhx_gis', '')
return self._root_rhx_gis or None
class RateController:
"""
Class providing request tracking and rate controlling to stay within rate limits.
It can be overridden to change Instaloader's behavior regarding rate limits, for example to raise a custom
exception when the rate limit is hit::
import instaloader
class MyRateController(instaloader.RateController):
def sleep(self, secs):
raise MyCustomException()
L = instaloader.Instaloader(rate_controller=lambda ctx: MyRateController(ctx))
"""
def __init__(self, context: InstaloaderContext):
self._context = context
self._query_timestamps = dict() # type: Dict[str, List[float]]
self._earliest_next_request_time = 0.0
def sleep(self, secs: float):
"""Wait given number of seconds."""
# Not static, to allow for the behavior of this method to depend on context-inherent properties, such as
# whether we are logged in.
# pylint:disable=no-self-use
time.sleep(secs)
def _dump_query_timestamps(self, current_time: float, failed_query_type: str):
windows = [10, 11, 20, 22, 30, 60]
self._context.error("Requests within last {} minutes grouped by type:"
.format('/'.join(str(w) for w in windows)),
repeat_at_end=False)
for query_type, times in self._query_timestamps.items():
reqs_in_sliding_window = [sum(t > current_time - w * 60 for t in times) for w in windows]
self._context.error(" {} {:>32}: {}".format(
"*" if query_type == failed_query_type else " ",
query_type,
" ".join("{:4}".format(reqs) for reqs in reqs_in_sliding_window)
), repeat_at_end=False)
def count_per_sliding_window(self, query_type: str) -> int:
"""Return how many requests can be done within the sliding window."""
# Not static, to allow for the count_per_sliding_window to depend on context-inherent properties, such as
# whether we are logged in.
# pylint:disable=no-self-use
return 75 if query_type in ['iphone', 'other'] else 200
def _reqs_in_sliding_window(self, query_type: Optional[str], current_time: float, window: float) -> List[float]:
if query_type is not None:
# timestamps of type query_type
relevant_timestamps = self._query_timestamps[query_type]
else:
# all GraphQL queries, i.e. not 'iphone' or 'other'
graphql_query_timestamps = filter(lambda tp: tp[0] not in ['iphone', 'other'],
self._query_timestamps.items())
relevant_timestamps = [t for times in (tp[1] for tp in graphql_query_timestamps) for t in times]
return list(filter(lambda t: t > current_time - window, relevant_timestamps))
def query_waittime(self, query_type: str, current_time: float, untracked_queries: bool = False) -> float:
"""Calculate time needed to wait before query can be executed."""
per_type_sliding_window = 660
if query_type not in self._query_timestamps:
self._query_timestamps[query_type] = []
self._query_timestamps[query_type] = list(filter(lambda t: t > current_time - 60 * 60,
self._query_timestamps[query_type]))
def per_type_next_request_time():
reqs_in_sliding_window = self._reqs_in_sliding_window(query_type, current_time, per_type_sliding_window)
if len(reqs_in_sliding_window) < self.count_per_sliding_window(query_type):
return 0.0
else:
return min(reqs_in_sliding_window) + per_type_sliding_window + 6
def gql_accumulated_next_request_time():
if query_type in ['iphone', 'other']:
return 0.0
gql_accumulated_sliding_window = 600
gql_accumulated_max_count = 275
reqs_in_sliding_window = self._reqs_in_sliding_window(None, current_time, gql_accumulated_sliding_window)
if len(reqs_in_sliding_window) < gql_accumulated_max_count:
return 0.0
else:
return min(reqs_in_sliding_window) + gql_accumulated_sliding_window
def untracked_next_request_time():
if untracked_queries:
reqs_in_sliding_window = self._reqs_in_sliding_window(query_type, current_time, per_type_sliding_window)
self._earliest_next_request_time = min(reqs_in_sliding_window) + per_type_sliding_window + 6
return self._earliest_next_request_time
return max(0.0,
max(
per_type_next_request_time(),
gql_accumulated_next_request_time(),
untracked_next_request_time(),
) - current_time)
def wait_before_query(self, query_type: str) -> None:
"""This method is called before a query to Instagram. It calls :meth:`RateController.sleep` to wait
until the request can be made."""
waittime = self.query_waittime(query_type, time.monotonic(), False)
assert waittime >= 0
if waittime > 15:
self._context.log("\nToo many queries in the last time. Need to wait {} seconds, until {:%H:%M}."
.format(round(waittime), datetime.now() + timedelta(seconds=waittime)))
if waittime > 0:
self.sleep(waittime)
if query_type not in self._query_timestamps:
self._query_timestamps[query_type] = [time.monotonic()]
else:
self._query_timestamps[query_type].append(time.monotonic())
def handle_429(self, query_type: str) -> None:
"""This method is called to handle a 429 Too Many Requests response. It calls :meth:`RateController.sleep` to
wait until we can repeat the same request."""
current_time = time.monotonic()
waittime = self.query_waittime(query_type, current_time, True)
assert waittime >= 0
self._dump_query_timestamps(current_time, query_type)
text_for_429 = ("Instagram responded with HTTP error \"429 - Too Many Requests\". Please do not run multiple "
"instances of Instaloader in parallel or within short sequence. Also, do not use any Instagram "
"App while Instaloader is running.")
self._context.error(textwrap.fill(text_for_429), repeat_at_end=False)
if waittime > 1.5:
self._context.error("The request will be retried in {} seconds, at {:%H:%M}."
.format(round(waittime), datetime.now() + timedelta(seconds=waittime)),
repeat_at_end=False)
if waittime > 0:
self.sleep(waittime)
+272
View File
@@ -0,0 +1,272 @@
import base64
import hashlib
import json
import os
from contextlib import contextmanager
from datetime import datetime, timedelta
from lzma import LZMAError
from typing import Any, Callable, Dict, Iterator, NamedTuple, Optional, Tuple, TypeVar
from .exceptions import InvalidArgumentException, QueryReturnedBadRequestException
from .instaloadercontext import InstaloaderContext
FrozenNodeIterator = NamedTuple('FrozenNodeIterator',
[('query_hash', str),
('query_variables', Dict),
('query_referer', Optional[str]),
('context_username', Optional[str]),
('total_index', int),
('best_before', Optional[float]),
('remaining_data', Optional[Dict])])
FrozenNodeIterator.__doc__ = \
"""A serializable representation of a :class:`NodeIterator` instance, saving its iteration state."""
FrozenNodeIterator.query_hash.__doc__ = """The GraphQL ``query_hash`` parameter."""
FrozenNodeIterator.query_variables.__doc__ = """The GraphQL ``query_variables`` parameter."""
FrozenNodeIterator.query_referer.__doc__ = """The HTTP referer used for the GraphQL query."""
FrozenNodeIterator.context_username.__doc__ = """The username who created the iterator, or ``None``."""
FrozenNodeIterator.total_index.__doc__ = """Number of items that have already been returned."""
FrozenNodeIterator.best_before.__doc__ = """Date when parts of the stored nodes might have expired."""
FrozenNodeIterator.remaining_data.__doc__ = \
"""The already-retrieved, yet-unprocessed ``edges`` and the ``page_info`` at time of freezing."""
T = TypeVar('T')
class NodeIterator(Iterator[T]):
"""
Iterate the nodes within edges in a GraphQL pagination. Instances of this class are returned by many (but not all)
of Instaloader's :class:`Post`-returning functions (such as :meth:`Profile.get_posts` etc.).
What makes this iterator special is its ability to freeze/store its current state, e.g. to interrupt an iteration,
and later thaw/resume from where it left off.
You can freeze a NodeIterator with :meth:`NodeIterator.freeze`::
post_iterator = profile.get_posts()
try:
for post in post_iterator:
do_something_with(post)
except KeyboardInterrupt:
save("resume_information.json", post_iterator.freeze())
and later reuse it with :meth:`NodeIterator.thaw` on an equally-constructed NodeIterator::
post_iterator = profile.get_posts()
post_iterator.thaw(load("resume_information.json"))
A :class:`FrozenNodeIterator` can only be thawn with a matching NodeIterator, i.e. a NodeIterator instance that has
been constructed with the same parameters as the instance that is represented by the :class:`FrozenNodeIterator` in
question. This is to ensure that an iteration cannot be resumed in a wrong, unmatching loop. As a quick way to
distinguish iterators that are saved e.g. in files, there is the :attr:`NodeIterator.magic` string: Two
NodeIterators are matching if and only if they have the same magic.
See also :func:`resumable_iteration` for a high-level context manager that handles a resumable iteration.
"""
_graphql_page_length = 50
_shelf_life = timedelta(days=29)
def __init__(self,
context: InstaloaderContext,
query_hash: str,
edge_extractor: Callable[[Dict[str, Any]], Dict[str, Any]],
node_wrapper: Callable[[Dict], T],
query_variables: Optional[Dict[str, Any]] = None,
query_referer: Optional[str] = None,
first_data: Optional[Dict[str, Any]] = None):
self._context = context
self._query_hash = query_hash
self._edge_extractor = edge_extractor
self._node_wrapper = node_wrapper
self._query_variables = query_variables if query_variables is not None else {}
self._query_referer = query_referer
self._page_index = 0
self._total_index = 0
if first_data is not None:
self._data = first_data
self._best_before = datetime.now() + NodeIterator._shelf_life
else:
self._data = self._query()
def _query(self, after: Optional[str] = None) -> Dict:
pagination_variables = {'first': NodeIterator._graphql_page_length} # type: Dict[str, Any]
if after is not None:
pagination_variables['after'] = after
try:
data = self._edge_extractor(
self._context.graphql_query(
self._query_hash, {**self._query_variables, **pagination_variables}, self._query_referer
)
)
self._best_before = datetime.now() + NodeIterator._shelf_life
return data
except QueryReturnedBadRequestException:
new_page_length = int(NodeIterator._graphql_page_length / 2)
if new_page_length >= 12:
NodeIterator._graphql_page_length = new_page_length
self._context.error("HTTP Error 400 (Bad Request) on GraphQL Query. Retrying with shorter page length.",
repeat_at_end=False)
return self._query(after)
else:
raise
def __iter__(self):
return self
def __next__(self) -> T:
if self._page_index < len(self._data['edges']):
node = self._data['edges'][self._page_index]['node']
page_index, total_index = self._page_index, self._total_index
try:
self._page_index += 1
self._total_index += 1
except KeyboardInterrupt:
self._page_index, self._total_index = page_index, total_index
raise
return self._node_wrapper(node)
if self._data['page_info']['has_next_page']:
query_response = self._query(self._data['page_info']['end_cursor'])
page_index, data = self._page_index, self._data
try:
self._page_index = 0
self._data = query_response
except KeyboardInterrupt:
self._page_index, self._data = page_index, data
raise
return self.__next__()
raise StopIteration()
@property
def count(self) -> Optional[int]:
"""The ``count`` as returned by Instagram. This is not always the total count this iterator will yield."""
return self._data.get('count') if self._data is not None else None
@property
def total_index(self) -> int:
"""Number of items that have already been returned."""
return self._total_index
@property
def magic(self) -> str:
"""Magic string for easily identifying a matching iterator file for resuming (hash of some parameters)."""
if 'blake2b' not in hashlib.algorithms_available:
magic_hash = hashlib.new('sha224')
else:
# Use blake2b when possible, i.e. on Python >= 3.6.
magic_hash = hashlib.blake2b(digest_size=6) # type:ignore # pylint: disable=no-member
magic_hash.update(json.dumps(
[self._query_hash, self._query_variables, self._query_referer, self._context.username]
).encode())
return base64.urlsafe_b64encode(magic_hash.digest()).decode()
def freeze(self) -> FrozenNodeIterator:
"""Freeze the iterator for later resuming."""
remaining_data = None
if self._data is not None:
remaining_data = {**self._data,
'edges': (self._data['edges'][(max(self._page_index - 1, 0)):])}
return FrozenNodeIterator(
query_hash=self._query_hash,
query_variables=self._query_variables,
query_referer=self._query_referer,
context_username=self._context.username,
total_index=max(self.total_index - 1, 0),
best_before=self._best_before.timestamp() if self._best_before else None,
remaining_data=remaining_data,
)
def thaw(self, frozen: FrozenNodeIterator) -> None:
"""
Use this iterator for resuming from earlier iteration.
:raises InvalidArgumentException:
If
- the iterator on which this method is called has already been used, or
- the given :class:`FrozenNodeIterator` does not match, i.e. belongs to a different iteration.
"""
if self._total_index or self._page_index:
raise InvalidArgumentException("thaw() called on already-used iterator.")
if (self._query_hash != frozen.query_hash or
self._query_variables != frozen.query_variables or
self._query_referer != frozen.query_referer or
self._context.username != frozen.context_username):
raise InvalidArgumentException("Mismatching resume information.")
if not frozen.best_before:
raise InvalidArgumentException("\"best before\" date missing.")
if frozen.remaining_data is None:
raise InvalidArgumentException("\"remaining_data\" missing.")
self._total_index = frozen.total_index
self._best_before = datetime.fromtimestamp(frozen.best_before)
self._data = frozen.remaining_data
@contextmanager
def resumable_iteration(context: InstaloaderContext,
iterator: Iterator,
load: Callable[[InstaloaderContext, str], Any],
save: Callable[[FrozenNodeIterator, str], None],
format_path: Callable[[str], str],
check_bbd: bool = True,
enabled: bool = True) -> Iterator[Tuple[bool, int]]:
"""
High-level context manager to handle a resumable iteration that can be interrupted with a KeyboardInterrupt.
It can be used as follows to automatically load a previously-saved state into the iterator, save the iterator's
state when interrupted, and delete the resume file upon completion::
post_iterator = profile.get_posts()
with resumable_iteration(
context=L.context,
iterator=post_iterator,
load=lambda _, path: FrozenNodeIterator(**json.load(open(path))),
save=lambda fni, path: json.dump(fni._asdict(), open(path, 'w')),
format_path=lambda magic: "resume_info_{}.json".format(magic)
) as (is_resuming, start_index):
for post in post_iterator:
do_something_with(post)
It yields a tuple (is_resuming, start_index).
When the passed iterator is not a :class:`NodeIterator`, it behaves as if ``resumable_iteration`` was not used,
just executing the inner body.
:param context: The :class:`InstaloaderContext`.
:param iterator: The fresh :class:`NodeIterator`.
:param load: Loads a FrozenNodeIterator from given path. The object is ignored if it has a different type.
:param save: Saves the given FrozenNodeIterator to the given path.
:param format_path: Returns the path to the resume file for the given magic.
:param check_bbd: Whether to check the best before date and reject an expired FrozenNodeIterator.
:param enabled: Set to False to disable all functionality and simply execute the inner body.
"""
if not enabled or not isinstance(iterator, NodeIterator):
yield False, 0
return
is_resuming = False
start_index = 0
resume_file_path = format_path(iterator.magic)
resume_file_exists = os.path.isfile(resume_file_path)
if resume_file_exists:
try:
fni = load(context, resume_file_path)
if not isinstance(fni, FrozenNodeIterator):
raise InvalidArgumentException("Invalid type.")
if check_bbd and fni.best_before and datetime.fromtimestamp(fni.best_before) < datetime.now():
raise InvalidArgumentException("\"Best before\" date exceeded.")
iterator.thaw(fni)
is_resuming = True
start_index = iterator.total_index
context.log("Resuming from {}.".format(resume_file_path))
except (InvalidArgumentException, LZMAError, json.decoder.JSONDecodeError) as exc:
context.error("Warning: Not resuming from {}: {}".format(resume_file_path, exc))
try:
yield is_resuming, start_index
except KeyboardInterrupt:
os.makedirs(os.path.dirname(resume_file_path), exist_ok=True)
save(iterator.freeze(), resume_file_path)
context.log("\nSaved resume information to {}.".format(resume_file_path))
raise
if resume_file_exists:
os.unlink(resume_file_path)
context.log("Iteration complete, deleted resume information file {}.".format(resume_file_path))
+635 -123
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -60,6 +60,7 @@ setup(
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3 :: Only',
'Topic :: Internet',
'Topic :: Multimedia :: Graphics'
+61 -18
View File
@@ -5,20 +5,25 @@ import shutil
import tempfile
import unittest
from itertools import islice
from typing import Optional
import instaloader
PROFILE_WITH_HIGHLIGHTS = 325732271
PUBLIC_PROFILE = "selenagomez"
PUBLIC_PROFILE_ID = 460563723
PUBLIC_PROFILE_WITH_IGTV = "natgeo"
HASHTAG = "kitten"
LOCATION = "362629379"
OWN_USERNAME = "aandergr"
NORMAL_MAX_COUNT = 2
PAGING_MAX_COUNT = 15
PRIVATE_PROFILE = "aandergr"
PRIVATE_PROFILE_ID = 1706625676
EMPTY_PROFILE = "not_public"
EMPTY_PROFILE_ID = 1928659031
# Preserve query timestamps (rate control) between tests to not get rate limited
instaloadercontext_query_timestamps = list()
ratecontroller = None # type: Optional[instaloader.RateController]
class TestInstaloaderAnonymously(unittest.TestCase):
@@ -31,11 +36,15 @@ class TestInstaloaderAnonymously(unittest.TestCase):
download_comments=True,
save_metadata=True)
self.L.context.raise_all_errors = True
self.L.context.query_timestamps = instaloadercontext_query_timestamps
if ratecontroller is not None:
# pylint:disable=protected-access
ratecontroller._context = self.L.context
self.L.context._rate_controller = ratecontroller
def tearDown(self):
global instaloadercontext_query_timestamps
instaloadercontext_query_timestamps = self.L.context.query_timestamps
# pylint:disable=global-statement,protected-access
global ratecontroller
ratecontroller = self.L.context._rate_controller
self.L.close()
os.chdir('/')
print("Removing {}".format(self.dir))
@@ -49,28 +58,47 @@ class TestInstaloaderAnonymously(unittest.TestCase):
self.assertTrue(post.date_utc < previous_post.date_utc)
previous_post = post
@unittest.SkipTest
@unittest.skip('')
def test_public_profile_download(self):
self.L.download_profile(PUBLIC_PROFILE, profile_pic=False, fast_update=True)
self.L.download_profile(PUBLIC_PROFILE, profile_pic=False, fast_update=True)
profiles = {self.L.check_profile_id(PUBLIC_PROFILE)}
self.L.download_profiles(profiles, profile_pic=False, fast_update=True, raise_errors=True)
self.L.download_profiles(profiles, profile_pic=False, fast_update=True, raise_errors=True)
def test_public_profile_paging(self):
self.post_paging_test(instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).get_posts())
def test_profile_pic_download(self):
self.L.download_profile(PUBLIC_PROFILE, profile_pic_only=True)
self.L.download_profiles({self.L.check_profile_id(PUBLIC_PROFILE)}, posts=False, raise_errors=True)
def test_hashtag_download(self):
self.L.download_hashtag(HASHTAG, NORMAL_MAX_COUNT)
def test_hashtag_paging(self):
for count, post in enumerate(instaloader.Hashtag.from_name(self.L.context, HASHTAG).get_all_posts()):
print(post)
if count == PAGING_MAX_COUNT:
break
def test_get_id_by_username(self):
self.assertEqual(PUBLIC_PROFILE_ID,
instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).userid)
def test_get_username_by_id(self):
def test_get_username_by_id_private(self):
self.assertEqual(PRIVATE_PROFILE.lower(),
instaloader.Profile.from_id(self.L.context, PRIVATE_PROFILE_ID).username)
def test_get_username_by_id_public(self):
self.assertEqual(PUBLIC_PROFILE.lower(),
instaloader.Profile.from_id(self.L.context, PUBLIC_PROFILE_ID).username)
def test_get_username_by_id_empty(self):
self.assertEqual(EMPTY_PROFILE.lower(),
instaloader.Profile.from_id(self.L.context, EMPTY_PROFILE_ID).username)
def test_get_username_by_name_empty(self):
self.assertEqual(EMPTY_PROFILE_ID,
instaloader.Profile.from_username(self.L.context, EMPTY_PROFILE).userid)
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)
@@ -82,6 +110,11 @@ class TestInstaloaderAnonymously(unittest.TestCase):
PAGING_MAX_COUNT):
print(post)
def test_public_profile_igtv(self):
for post in islice(instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE_WITH_IGTV).get_igtv_posts(),
PAGING_MAX_COUNT):
print(post)
class TestInstaloaderLoggedIn(TestInstaloaderAnonymously):
@@ -89,13 +122,13 @@ class TestInstaloaderLoggedIn(TestInstaloaderAnonymously):
super().setUp()
self.L.load_session_from_file(OWN_USERNAME)
@unittest.SkipTest
@unittest.skip('')
def test_stories_download(self):
self.L.download_stories()
@unittest.SkipTest
@unittest.skip('')
def test_private_profile_download(self):
self.L.download_profile(PRIVATE_PROFILE, download_stories=True)
self.L.download_profiles({self.L.check_profile_id(PRIVATE_PROFILE)}, stories=True, raise_errors=True)
def test_stories_paging(self):
for user_story in self.L.get_stories():
@@ -137,8 +170,8 @@ class TestInstaloaderLoggedIn(TestInstaloaderAnonymously):
def test_followees_and_stories(self):
profile = instaloader.Profile.from_username(self.L.context, OWN_USERNAME)
for f in islice(profile.get_followees(), PAGING_MAX_COUNT):
self.L.download_profile(f.username, profile_pic=False, download_stories_only=True)
followees = set(islice(profile.get_followees(), PAGING_MAX_COUNT))
self.L.download_profiles(followees, profile_pic=False, stories=True, posts=False, raise_errors=True)
def test_get_followees(self):
profile = instaloader.Profile.from_username(self.L.context, OWN_USERNAME)
@@ -150,9 +183,10 @@ class TestInstaloaderLoggedIn(TestInstaloaderAnonymously):
for f in profile.get_followers():
print(f.username)
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_get_similar_accounts(self):
profile = instaloader.Profile.from_username(self.L.context, OWN_USERNAME)
for f in profile.get_similar_accounts():
print(f.username)
def test_get_likes(self):
for post in instaloader.Profile.from_username(self.L.context, OWN_USERNAME).get_posts():
@@ -166,6 +200,15 @@ class TestInstaloaderLoggedIn(TestInstaloaderAnonymously):
if count == PAGING_MAX_COUNT:
break
def test_location_download(self):
self.L.download_location(LOCATION, NORMAL_MAX_COUNT)
def test_location_paging(self):
for count, post in enumerate(self.L.get_location_posts(LOCATION)):
print(post)
if count == PAGING_MAX_COUNT:
break
if __name__ == '__main__':
unittest.main()