Compare commits

...
33 Commits
Author SHA1 Message Date
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
36 changed files with 655 additions and 832 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ Steps to reproduce the behavior:
(e.g. Instaloader command line) (e.g. Instaloader command line)
**Expected behavior** **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** **Error messages and tracebacks**
If applicable, add error messages and tracebacks to help explain your problem. If applicable, add error messages and tracebacks to help explain your problem.
+1 -1
View File
@@ -14,7 +14,7 @@ A clear and concise description of what you want to happen.
**Describe alternatives you've considered** **Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features 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 PR?** **If the feature request is accepted, would you be willing to submit a pull request?**
Yes / No Yes / No
<!-- Help can be provided if you need assistance submitting a PR --> <!-- Help can be provided if you need assistance submitting a PR -->
+1 -1
View File
@@ -6,7 +6,7 @@ verify_ssl = true
[dev-packages] [dev-packages]
pylint = "*" pylint = "*"
mypy = "*" mypy = "*"
sphinx = "*" sphinx = "~=2.3"
pyinstaller = "*" pyinstaller = "*"
pefile = "*" pefile = "*"
pywin32-ctypes = "*" pywin32-ctypes = "*"
Generated
+108 -100
View File
@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "33408a60fda1a56bf680462fad21643ae0159bc13f3b85d8f10ebf3eef7d8725" "sha256": "58caf1be034bed9aa7ebb8c78e75ffa7d7abe820d014b6398c444a5db566b306"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": {}, "requires": {},
@@ -16,10 +16,10 @@
"default": { "default": {
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
"sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
], ],
"version": "==2019.11.28" "version": "==2020.6.20"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
@@ -30,25 +30,25 @@
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
], ],
"version": "==2.8" "version": "==2.10"
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.22.0" "version": "==2.24.0"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
"sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
], ],
"version": "==1.25.8" "version": "==1.25.10"
} }
}, },
"develop": { "develop": {
@@ -68,10 +68,10 @@
}, },
"astroid": { "astroid": {
"hashes": [ "hashes": [
"sha256:71ea07f44df9568a75d0f354c49143a4575d90645e9fead6dfb52c26a85ed13a", "sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703",
"sha256:840947ebfa8b58f318d42301cf8c0a20fd794a33b61cc4638e28e9e61ba32f42" "sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"
], ],
"version": "==2.3.3" "version": "==2.4.2"
}, },
"babel": { "babel": {
"hashes": [ "hashes": [
@@ -82,10 +82,10 @@
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3", "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
"sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f" "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
], ],
"version": "==2019.11.28" "version": "==2020.6.20"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
@@ -109,10 +109,10 @@
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
], ],
"version": "==2.8" "version": "==2.10"
}, },
"imagesize": { "imagesize": {
"hashes": [ "hashes": [
@@ -130,10 +130,10 @@
}, },
"jinja2": { "jinja2": {
"hashes": [ "hashes": [
"sha256:93187ffbc7808079673ef52771baa950426fd664d3aad1d0fa3e95644360e250", "sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
"sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49" "sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
], ],
"version": "==2.11.1" "version": "==2.11.2"
}, },
"lazy-object-proxy": { "lazy-object-proxy": {
"hashes": [ "hashes": [
@@ -208,23 +208,23 @@
}, },
"mypy": { "mypy": {
"hashes": [ "hashes": [
"sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a", "sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c",
"sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7", "sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86",
"sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2", "sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b",
"sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474", "sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd",
"sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0", "sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc",
"sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217", "sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea",
"sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749", "sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e",
"sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6", "sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308",
"sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf", "sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406",
"sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36", "sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d",
"sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b", "sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707",
"sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72", "sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d",
"sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1", "sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c",
"sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1" "sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.761" "version": "==0.782"
}, },
"mypy-extensions": { "mypy-extensions": {
"hashes": [ "hashes": [
@@ -235,10 +235,10 @@
}, },
"packaging": { "packaging": {
"hashes": [ "hashes": [
"sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73", "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
"sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334" "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
], ],
"version": "==20.1" "version": "==20.4"
}, },
"pefile": { "pefile": {
"hashes": [ "hashes": [
@@ -249,27 +249,27 @@
}, },
"psutil": { "psutil": {
"hashes": [ "hashes": [
"sha256:094f899ac3ef72422b7e00411b4ed174e3c5a2e04c267db6643937ddba67a05b", "sha256:0ee3c36428f160d2d8fce3c583a0353e848abb7de9732c50cf3356dd49ad63f8",
"sha256:10b7f75cc8bd676cfc6fa40cd7d5c25b3f45a0e06d43becd7c2d2871cbb5e806", "sha256:10512b46c95b02842c225f58fa00385c08fa00c68bac7da2d9a58ebe2c517498",
"sha256:1b1575240ca9a90b437e5a40db662acd87bbf181f6aa02f0204978737b913c6b", "sha256:4080869ed93cce662905b029a1770fe89c98787e543fa7347f075ade761b19d6",
"sha256:21231ef1c1a89728e29b98a885b8e0a8e00d09018f6da5cdc1f43f988471a995", "sha256:5e9d0f26d4194479a13d5f4b3798260c20cecf9ac9a461e718eb59ea520a360c",
"sha256:28f771129bfee9fc6b63d83a15d857663bbdcae3828e1cb926e91320a9b5b5cd", "sha256:66c18ca7680a31bf16ee22b1d21b6397869dda8059dbdb57d9f27efa6615f195",
"sha256:70387772f84fa5c3bb6a106915a2445e20ac8f9821c5914d7cbde148f4d7ff73", "sha256:68d36986ded5dac7c2dcd42f2682af1db80d4bce3faa126a6145c1637e1b559f",
"sha256:b560f5cd86cf8df7bcd258a851ca1ad98f0d5b8b98748e877a0aec4e9032b465", "sha256:90990af1c3c67195c44c9a889184f84f5b2320dce3ee3acbd054e3ba0b4a7beb",
"sha256:b74b43fecce384a57094a83d2778cdfc2e2d9a6afaadd1ebecb2e75e0d34e10d", "sha256:a5b120bb3c0c71dfe27551f9da2f3209a8257a178ed6c628a819037a8df487f1",
"sha256:e85f727ffb21539849e6012f47b12f6dd4c44965e56591d8dec6e8bc9ab96f4a", "sha256:d8a82162f23c53b8525cf5f14a355f5d1eea86fa8edde27287dd3a98399e4fdf",
"sha256:fd2e09bb593ad9bdd7429e779699d2d47c1268cbde4dda95fcd1bd17544a0217", "sha256:f2018461733b23f308c298653c8903d32aaad7873d25e1d228765e91ae42c3f2",
"sha256:ffad8eb2ac614518bbe3c0b8eb9dffdb3a8d2e3a7d5da51c5b974fb723a5c5aa" "sha256:ff1977ba1a5f71f89166d5145c3da1cea89a0fdb044075a12c720ee9123ec818"
], ],
"index": "pypi", "index": "pypi",
"version": "==5.6.7" "version": "==5.7.2"
}, },
"pygments": { "pygments": {
"hashes": [ "hashes": [
"sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b", "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44",
"sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe" "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"
], ],
"version": "==2.5.2" "version": "==2.6.1"
}, },
"pyinstaller": { "pyinstaller": {
"hashes": [ "hashes": [
@@ -280,25 +280,25 @@
}, },
"pylint": { "pylint": {
"hashes": [ "hashes": [
"sha256:3db5468ad013380e987410a8d6956226963aed94ecb5f9d3a28acca6d9ac36cd", "sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc",
"sha256:886e6afc935ea2590b462664b161ca9a5e40168ea99e5300935f6591ad467df4" "sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.4.4" "version": "==2.5.3"
}, },
"pyparsing": { "pyparsing": {
"hashes": [ "hashes": [
"sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f", "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec" "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
], ],
"version": "==2.4.6" "version": "==2.4.7"
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", "sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" "sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"
], ],
"version": "==2019.3" "version": "==2020.1"
}, },
"pywin32-ctypes": { "pywin32-ctypes": {
"hashes": [ "hashes": [
@@ -310,18 +310,18 @@
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4", "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31" "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.22.0" "version": "==2.24.0"
}, },
"six": { "six": {
"hashes": [ "hashes": [
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a", "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c" "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
], ],
"version": "==1.14.0" "version": "==1.15.0"
}, },
"snowballstemmer": { "snowballstemmer": {
"hashes": [ "hashes": [
@@ -332,32 +332,32 @@
}, },
"sphinx": { "sphinx": {
"hashes": [ "hashes": [
"sha256:298537cb3234578b2d954ff18c5608468229e116a9757af3b831c2b2b4819159", "sha256:b4c750d546ab6d7e05bdff6ac24db8ae3e8b8253a3569b754e445110a0a12b66",
"sha256:e6e766b74f85f37a5f3e0773a1e1be8db3fcb799deb58ca6d18b70b0b44542a5" "sha256:fc312670b56cb54920d6cc2ced455a22a547910de10b3142276495ced49231cb"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.3.1" "version": "==2.4.4"
}, },
"sphinxcontrib-applehelp": { "sphinxcontrib-applehelp": {
"hashes": [ "hashes": [
"sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897", "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a",
"sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d" "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"
], ],
"version": "==1.0.1" "version": "==1.0.2"
}, },
"sphinxcontrib-devhelp": { "sphinxcontrib-devhelp": {
"hashes": [ "hashes": [
"sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34", "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e",
"sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981" "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"
], ],
"version": "==1.0.1" "version": "==1.0.2"
}, },
"sphinxcontrib-htmlhelp": { "sphinxcontrib-htmlhelp": {
"hashes": [ "hashes": [
"sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422", "sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f",
"sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7" "sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"
], ],
"version": "==1.0.2" "version": "==1.0.3"
}, },
"sphinxcontrib-jsmath": { "sphinxcontrib-jsmath": {
"hashes": [ "hashes": [
@@ -368,17 +368,24 @@
}, },
"sphinxcontrib-qthelp": { "sphinxcontrib-qthelp": {
"hashes": [ "hashes": [
"sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20", "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72",
"sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f" "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"
], ],
"version": "==1.0.2" "version": "==1.0.3"
}, },
"sphinxcontrib-serializinghtml": { "sphinxcontrib-serializinghtml": {
"hashes": [ "hashes": [
"sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227", "sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc",
"sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768" "sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"
], ],
"version": "==1.1.3" "version": "==1.1.4"
},
"toml": {
"hashes": [
"sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
],
"version": "==0.10.1"
}, },
"typed-ast": { "typed-ast": {
"hashes": [ "hashes": [
@@ -404,28 +411,29 @@
"sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
"sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
], ],
"markers": "implementation_name == 'cpython' and python_version < '3.8'",
"version": "==1.4.1" "version": "==1.4.1"
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:091ecc894d5e908ac75209f10d5b4f118fbdb2eb1ede6a63544054bb1edb41f2", "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5",
"sha256:910f4656f54de5993ad9304959ce9bb903f90aadc7c67a0bef07e678014e892d", "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae",
"sha256:cf8b63fedea4d89bab840ecbb93e75578af28f76f66c35889bd7065f5af88575" "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"
], ],
"version": "==3.7.4.1" "version": "==3.7.4.2"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
"sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
], ],
"version": "==1.25.8" "version": "==1.25.10"
}, },
"wrapt": { "wrapt": {
"hashes": [ "hashes": [
"sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1" "sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"
], ],
"version": "==1.11.2" "version": "==1.12.1"
} }
} }
} }
+2 -1
View File
@@ -27,8 +27,9 @@
:alt: Arch User Repository Package :alt: Arch User Repository Package
:target: https://aur.archlinux.org/packages/instaloader/ :target: https://aur.archlinux.org/packages/instaloader/
.. |downloads| image:: https://img.shields.io/pypi/dm/instaloader.svg .. |downloads| image:: https://pepy.tech/badge/instaloader/month
:alt: PyPI Download Count :alt: PyPI Download Count
:target: https://pepy.tech/project/instaloader/month
.. badges-end .. badges-end
+2 -2
View File
@@ -17,7 +17,7 @@ import subprocess
def __main(): def __main():
with contextlib.suppress(AttributeError, psutil.Error): with contextlib.suppress(AttributeError, psutil.Error):
if psutil.Process().parent().parent().name() == "explorer.exe": if psutil.Process().parent().parent().name() == "explorer.exe":
subprocess.Popen("powershell -NoExit -Command \\\"& {0}\\\"".format(sys.argv[0])) subprocess.Popen("powershell -NoExit -Command \\\"& '{0}'\\\"".format(sys.argv[0]))
return return
main() main()
@@ -44,7 +44,7 @@ with open('__main__.py', 'w+') as f:
f.writelines(lines) f.writelines(lines)
# install dependencies and invoke PyInstaller # install dependencies and invoke PyInstaller
commands = ["pip install pipenv==2018.11.26", commands = ["pip install pipenv==2020.6.2",
"pipenv sync --dev", "pipenv sync --dev",
"pipenv run pyinstaller --log-level=DEBUG instaloader.spec"] "pipenv run pyinstaller --log-level=DEBUG instaloader.spec"]
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-78
View File
@@ -1,78 +0,0 @@
@import url(bootstrap-4.1.3.min.css);
a {
color: #008d06; }
a:hover, a.current, a.active {
color: #f48400; }
a.current, a.active {
font-weight: 500; }
.doc-sidebar a:hover, .modal-body a:hover {
text-decoration: none; }
code {
color: #212529; }
.highlight {
border-radius: 0.3em; }
.highlight pre {
padding: 0.7em;
color: #fff; }
.highlight .c1 {
color: #666; }
.highlight .k, .highlight .kn, .highlight .ow {
color: #008d06; }
.highlight .nb, .highlight .ne, .highlight .nf, .highlight .vm {
color: #f48400; }
.highlight .s1, .highlight .s2, .highlight .sa, .highlight .se, .highlight .si, .highlight .mi {
color: #ff7efd; }
.highlight .c1, .highlight .nf, .highlight .sa, .highlight .se, .highlight .si {
font-style: italic; }
.highlight .k, .highlight .kn, .highlight .ow {
font-weight: bold; }
.doc-content {
padding-top: 0.5em;
top: 3.5625em;
position: fixed;
bottom: 0;
overflow-y: auto; }
.doc-sidebar {
position: fixed;
padding-top: 0.5em;
top: 3.5625em;
bottom: 0;
right: 0;
overflow-y: auto; }
.doc-sidebar ul {
list-style-type: none;
padding-left: 1em; }
.doc-sidebar ul.current {
padding-left: 0; }
.doc-sidebar li {
padding-top: 0.2em;
padding-bottom: 0.2em; }
.admonition {
border-radius: 0.3em;
border: 1px solid #ccc;
background-color: #f8f9fa; }
.admonition .admonition-title {
color: #ff7efd; }
.topic {
border-radius: 0.3em;
background-color: #f8f9fa; }
.nav-link {
padding-top: 0;
padding-bottom: 0; }
.btn-success {
background-color: #008d06;
border-color: #008d06; }
.btn-success:hover {
background-color: #f48400;
border-color: #f48400; }
/*# sourceMappingURL=instaloader.css.map */
-22
View File
@@ -1,22 +0,0 @@
$(function () {
$('[data-toggle="tooltip"]').tooltip();
$('.doc-sidebar > ul > li.current').attr("id", "localtoc");
$('#localtoc ul').addClass("nav flex-column");
$('#localtoc ul li').addClass("nav-item");
$('#localtoc ul li a').addClass("nav-link");
$('.doc-content').scrollspy({target: '#localtoc'});
const top_href = '#' + $('.section:first').attr("id");
$('#localtoc > a.current').attr("href", top_href);
$('#navbarToc a').on("click", function () {
const href = $(this).attr("href");
if (href === '#') {
window.location.href = top_href;
} else {
window.location.href = href;
}
$('#navbarToc').modal('hide');
});
});
-124
View File
@@ -1,124 +0,0 @@
@import 'bootstrap-4.1.3.min.css';
$color_instaloader_main: #008d06;
$color_instaloader_accent: #f48400;
$color_instaloader_hyper: #ff7efd;
$color_normal_text: #212529;
$color_light_bg: #f8f9fa;
a {
color: $color_instaloader_main;
&:hover, &.current, &.active {
color: $color_instaloader_accent;
}
&.current, &.active {
font-weight: 500;
}
}
.doc-sidebar, .modal-body {
a:hover {
text-decoration: none
}
}
code {
color: $color_normal_text;
}
.highlight {
border-radius: 0.3em;
pre {
padding: 0.7em;
color: #fff;
}
.c1 {
color: #666;
}
.k, .kn, .ow {
color: $color_instaloader_main
}
.nb, .ne, .nf, .vm {
color: $color_instaloader_accent
}
.s1, .s2, .sa, .se, .si, .mi {
color: $color_instaloader_hyper
}
.c1, .nf, .sa, .se, .si {
font-style: italic;
}
.k, .kn, .ow {
font-weight: bold
}
}
.doc-content {
padding-top: 0.5em;
top: 3.5625em;
position: fixed;
bottom: 0;
overflow-y: auto;
}
.doc-sidebar {
position: fixed;
padding-top: 0.5em;
top: 3.5625em;
bottom: 0;
right: 0;
overflow-y: auto;
ul {
list-style-type: none;
padding-left: 1em;
&.current {
padding-left: 0;
}
}
li {
padding-top: 0.2em;
padding-bottom: 0.2em;
}
}
.admonition {
border-radius: 0.3em;
border: 1px solid #ccc;
background-color: $color_light_bg;
.admonition-title {
color: $color_instaloader_hyper;
}
}
.topic {
border-radius: 0.3em;
background-color: $color_light_bg;
}
.nav-link {
padding-top: 0;
padding-bottom: 0;
}
.btn-success {
background-color: $color_instaloader_main;
border-color: $color_instaloader_main;
&:hover {
background-color: $color_instaloader_accent;
border-color: $color_instaloader_accent;
}
}
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
+46 -78
View File
@@ -1,9 +1,6 @@
{% extends "!layout.html" %} {% extends "!layout.html" %}
{% set html5_doctype = True %} {% set html5_doctype = True %}
{% set script_files = script_files + ['_static/instaloaderdoc.js'] %}
{% set script_files = script_files + ['_static/bootstrap-4.1.3.bundle.min.js', '_static/instaloader.js'] %}
{% block htmltitle %} {% block htmltitle %}
{% if pagename == "index" %} {% if pagename == "index" %}
<title>{{ title|striptags|e }}</title> <title>{{ title|striptags|e }}</title>
@@ -11,7 +8,6 @@
<title>{{ title|striptags|e }}{{ titlesuffix }}</title> <title>{{ title|striptags|e }}{{ titlesuffix }}</title>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block extrahead %} {% block extrahead %}
{{ super() }} {{ super() }}
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
@@ -22,94 +18,66 @@
<link rel="canonical" href="https://instaloader.github.io/{{ pagename }}.html"/> <link rel="canonical" href="https://instaloader.github.io/{{ pagename }}.html"/>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block header %} {% block header %}
<nav class="navbar navbar-expand-sm navbar-light bg-light fixed-top border-bottom"> <nav class="navbar navbar-light navbar-expand bg-light border-bottom">
<a class="navbar-brand" href="{{ pathto(master_doc) }}"> <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"> <img src="{{ pathto('_static/' + logo, 1) }}" width="30" height="30" class="d-inline-block align-top" alt="logo">
Instaloader <span class="ml-3">Instaloader</span>
</a> </a>
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav mr-auto">
<li class="nav-item ml-0 ml-sm-2 ml-md-0"> <li class="nav-item ml-0 ml-sm-2">
<a target="_blank" class="nav-link" <a class="nav-link"
href="https://github.com/instaloader/instaloader/releases/tag/v{{ current_release }}" 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> title="Released on {{ current_release_date }}" data-toggle="tooltip">
v<b>{{ current_release }}</b>
</a>
</li> </li>
</ul> </ul>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarLinks" <ul class="navbar-nav ml-auto d-none d-sm-flex">
aria-controls="navbarLinks" aria-expanded="false" aria-label="Toggle navigation"> <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> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarLinks">
<ul class="navbar-nav ml-auto">
<li class="nav-item">
<a target="_blank" class="nav-link" href="https://github.com/instaloader/instaloader/issues">Issues</a>
</li>
<li class="nav-item">
<a target="_blank" class="nav-link" href="https://github.com/instaloader/instaloader/releases">Releases</a>
</li>
<li class="nav-item">
<a target="_blank" class="nav-link" href="https://github.com/instaloader/instaloader">GitHub</a>
</li>
</ul>
<form class="form-inline" action="search.html" method="get">
<div class="input-group my-2 my-sm-0 ml-2">
<input class="form-control" type="search" placeholder="Search" aria-label="Search" name="q">
<div class="input-group-append">
<button class="btn btn-success btn-small" type="submit">Go</button>
</div>
</div>
</form>
</div>
{% if pagename != "index" %}
<button type="button" class="btn btn-success d-md-none btn-small ml-2" data-toggle="modal"
data-target="#navbarToc">
TOC
</button>
{% endif %}
</nav> </nav>
{% if pagename != "index" %}
<div class="modal fade" id="navbarToc" tabindex="-1" role="dialog"
aria-hidden="true" aria-labelledby="navbarTocTitle">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="navbarTocTitle">Contents</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{{ toctree(maxdepth=-1) }}
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}
{% block relbar1 %}{% endblock %} {% block relbar1 %}{% endblock %}
{% block relbar2 %}{% endblock %} {% block relbar2 %}{% endblock %}
{% block footer %}{% endblock %} {% block footer %}{% endblock %}
{% block content %} {% block content %}
<div class="container-fluid row"> <div class="container-fluid">
<div class="col-12 {% if pagename != "index" %}col-md-9 col-xl-10{% endif %} doc-content"> <div class="row">
{% block body %}{% endblock %} <div class="instaloaderdoc-sidebar collapse d-md-block col-md-3 col-xl-2 bg-light"
</div> id="instaloaderdocSidebar">
{% if pagename != "index" %} <nav role="navigation" aria-label="Table of Contents">
<div class="d-none col-md-3 col-xl-2 bg-light doc-sidebar d-md-flex flex-column border-left"> <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>
<h3>{{ _('Table of Contents') }}</h3> <input type="search" name="q" id="searchQueryInput" class="form-control my-2"
{{ toctree(maxdepth=-1) }} placeholder="Search..." aria-label="Search">
</form>
<div class="d-flex justify-content-between small mt-auto mb-2"> <h4>Contents</h4>
<a class="reference internal{% if pagename == "genindex" %} current{% endif %}" href="genindex.html">Index</a> {{ toctree(maxdepth=-1) }}
{% if pagename != "genindex" and pagename != "search" %} </nav>
<a class="reference external"
href="https://github.com/instaloader/instaloader/edit/master/docs/{{ pagename }}.rst">Edit this page</a>
{% endif %}
</div>
</div> </div>
{% endif %} <main class="col-12 col-md-9 col-xl-10 pt-2" role="main">
{% block body %}{% endblock %}
</main>
</div>
</div> </div>
{% endblock %} {% endblock %}
+14 -164
View File
@@ -12,11 +12,8 @@ Python Module :mod:`instaloader`
.. highlight:: python .. highlight:: python
.. contents::
:backlinks: none
Instaloader exposes its internally used methods and structures as a Python 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. allowing to further customize obtaining media and metadata.
Start with getting an instance of :class:`Instaloader`:: Start with getting an instance of :class:`Instaloader`::
@@ -85,173 +82,26 @@ metadata of a Profile. :class:`Profile` instances can be created with:
- :meth:`Profile.get_followers` - :meth:`Profile.get_followers`
Profiles that follow given user. 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. Owner profile of particular object.
- :meth:`Post.get_likes` - :meth:`Post.get_likes`
Profiles that liked a given :class:`Post` 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. Profile of a Post comment.
A reference of the many methods provided by the :mod:`instaloader` module is For a list of a few code examples that use the Instaloader module, see
provided in the remainder of this document. :ref:`codesnippets`.
For a list of real code examples using the Instaloader module for advanced The reference of the classes and functions provided by the :mod:`instaloader` module is
tasks, see :ref:`codesnippets`. divided into the following subsections:
``Instaloader`` (Main Class) .. toctree::
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :maxdepth: 2
.. autoclass:: Instaloader module/instaloader
:no-show-inheritance: module/structures
module/nodeiterator
Instagram Structures module/instaloadercontext
^^^^^^^^^^^^^^^^^^^^ module/exceptions
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` and :class:`Hashtag` 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:: 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
Resumable Iterations
^^^^^^^^^^^^^^^^^^^^
.. versionadded:: 4.5
.. autoclass:: NodeIterator
:no-show-inheritance:
.. autoclass:: FrozenNodeIterator
:no-show-inheritance:
.. autofunction:: resumable_iteration
``InstaloaderContext`` (Low-level functions)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
``InstaloaderContext``
""""""""""""""""""""""
.. autoclass:: InstaloaderContext
:no-show-inheritance:
``RateController``
""""""""""""""""""
.. autoclass:: RateController
:no-show-inheritance:
.. versionadded:: 4.5
+17 -16
View File
@@ -77,7 +77,7 @@ Instaloader supports the following targets:
instructs Instaloader to also **download the user's stories**, instructs Instaloader to also **download the user's stories**,
- :option:`--highlights` - :option:`--highlights`
to **download highlights of each profile that is downloaded**, to **download the highlights of that profile**,
- :option:`--tagged` - :option:`--tagged`
to **download posts where the user is tagged**, and to **download posts where the user is tagged**, and
@@ -86,7 +86,7 @@ Instaloader supports the following targets:
to **download IGTV videos**. to **download IGTV videos**.
- ``"#hashtag"`` - ``"#hashtag"``
Posts with a certain **hashtag** (the quotes are usually necessary), Posts with a certain **hashtag** (the quotes are usually necessary).
- ``%location id`` - ``%location id``
Posts tagged with a given location; the location ID is the numerical ID Posts tagged with a given location; the location ID is the numerical ID
@@ -98,20 +98,20 @@ Instaloader supports the following targets:
- ``:stories`` - ``:stories``
The currently-visible **stories** of your followees (requires The currently-visible **stories** of your followees (requires
:option:`--login`), :option:`--login`).
- ``:feed`` - ``:feed``
Your **feed** (requires :option:`--login`), Your **feed** (requires :option:`--login`).
- ``:saved`` - ``:saved``
Posts which are marked as **saved** (requires :option:`--login`), Posts which are marked as **saved** (requires :option:`--login`).
- ``@profile`` - ``@profile``
All profiles that are followed by ``profile``, i.e. the *followees* of All profiles that are followed by ``profile``, i.e. the *followees* of
``profile`` (requires :option:`--login`). ``profile`` (requires :option:`--login`).
- ``-post`` - ``-post``
Replace **post** with the post's shortcode to download single post. Must be preceeded by ``--`` in 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:: the argument list to not be mistaken as an option flag::
instaloader -- -B_K4CykAOtf instaloader -- -B_K4CykAOtf
@@ -196,7 +196,7 @@ Filter Posts
.. py:currentmodule:: instaloader .. py:currentmodule:: instaloader
The options :option:`--post-filter` and :option:`--storyitem-filter` 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. meet to be downloaded. If not given, all posts are downloaded.
The filter string must be a The filter string must be a
@@ -208,7 +208,7 @@ Id est, the following attributes can be used with both
:option:`--post-filter` and :option:`--storyitem-filter`: :option:`--post-filter` and :option:`--storyitem-filter`:
- :attr:`~Post.owner_username` (str), :attr:`~Post.owner_id` (int) - :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) - :attr:`~Post.date_utc` (datetime), :attr:`~Post.date_local` (datetime)
Creation timestamp. Since :class:`~datetime.datetime` objects can be created Creation timestamp. Since :class:`~datetime.datetime` objects can be created
@@ -217,7 +217,7 @@ Id est, the following attributes can be used with both
instaloader --post-filter="date_utc <= datetime(2018, 5, 31)" target instaloader --post-filter="date_utc <= datetime(2018, 5, 31)" target
- :attr:`~Post.is_video` (bool) - :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 instaloader --post-filter="not is_video" target
@@ -234,8 +234,8 @@ As :option:`--post-filter`, the following attributes can be used additionally:
instaloader --login=your_username --post-filter=viewer_has_liked :feed instaloader --login=your_username --post-filter=viewer_has_liked :feed
- :attr:`~Post.likes` (int), :attr:`~Post.comments` (int) - :attr:`~Post.likes` (int), :attr:`~Post.comments` (int)
Likes count / Comments count. You might only want to download posts that Likes count / comments count. You might only want to download posts that
either you liked or were liked by many others**:: were either liked by yourself or by many others::
instaloader --login=your_username --post-filter="likes>100 or viewer_has_liked" profile instaloader --login=your_username --post-filter="likes>100 or viewer_has_liked" profile
@@ -265,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 You can customize what metadata to save for each Post or StoryItem with
:option:`--post-metadata-txt` and :option:`--storyitem-metadata-txt`. The :option:`--post-metadata-txt` and :option:`--storyitem-metadata-txt`. The
default is ``--post-metadata-txt={caption}`` and no storyitem metadata txt. 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. the result is saved in text files, unless it is empty.
Specifying these options multiple times results in output having multiple lines, Specifying these options multiple times results in output having multiple lines,
@@ -299,10 +299,10 @@ Instaloader logged-in, create a session file::
instaloader --login=your_username instaloader --login=your_username
Then use the same parameter 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:: the given targets::
instaloader --login=your_username --quiet <target> [...] instaloader --login=your_username --quiet target [...]
Instaloader saves the session file to Instaloader saves the session file to
``~/.config/instaloader/session-YOUR-USERNAME``. See ``~/.config/instaloader/session-YOUR-USERNAME``. See
@@ -317,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 powerful and intuitive Python API for Instagram, allowing to further customize
obtaining media and metadata. obtaining media and metadata.
Also see :ref:`codesnippets`, where we collect example scripts that use Also see :ref:`codesnippets`, where we collect a few example scripts that use
Instaloader to achieve more complex tasks. Instaloader for simple tasks that cannot be done with the command line
interface.
+12 -46
View File
@@ -36,7 +36,7 @@ stores all posts along with the pictures's captions there.
Re-Download the given object Re-Download the given object
- ``+args.txt`` - ``+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 What to Download of each Post
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -59,7 +59,7 @@ What to Download of each Post
.. option:: --geotags, -G .. 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 the location's name and a Google Maps link. This requires an additional
request to the Instagram server for each picture. Requires :option:`--login`. request to the Instagram server for each picture. Requires :option:`--login`.
@@ -94,13 +94,6 @@ What to Download of each Post
What to Download of each Profile 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 .. option:: --no-posts
Do not download regular posts. Do not download regular posts.
@@ -113,12 +106,12 @@ What to Download of each Profile
.. option:: --stories, -s .. 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:`--login`.
.. option:: --highlights .. option:: --highlights
Also **download highlights** of each profile that is downloaded. Requires Also download highlights of each profile that is downloaded. Requires
:option:`--login`. :option:`--login`.
.. versionadded:: 4.1 .. versionadded:: 4.1
@@ -135,20 +128,6 @@ What to Download of each Profile
.. versionadded:: 4.3 .. versionadded:: 4.3
.. option:: --stories-only
.. deprecated:: 4.1
Use :option:`--stories` :option:`--no-posts`.
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.
Which Posts to Download Which Posts to Download
^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
@@ -182,7 +161,7 @@ Which Posts to Download
Login (Download Private Profiles) 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 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 password!) will be saved to a local file to be reused next time you want
Instaloader to login. Instaloader to login.
@@ -257,19 +236,6 @@ How to Download
to ``3``. If a connection fails, it can be manually skipped by hitting to ``3``. If a connection fails, it can be manually skipped by hitting
:kbd:`Control-c`. Set this to ``0`` to retry infinitely. :kbd:`Control-c`. Set this to ``0`` to retry infinitely.
.. option:: --commit-mode
Tries to ensure downloaded images avoid corruption in case of unexpected
interruption. If the last picture is corrupted, Instaloader will fix the
picture the next time it is run.
.. versionadded:: 4.2
.. warning::
We discourage from using the "commit mode" due to its bugs when being used
with other command line options (e.g. :issue:`257` with
:option:`--fast-update` and :issue:`483` with :option:`--tagged`).
.. option:: --request-timeout N .. option:: --request-timeout N
Seconds to wait before timing out a connection request. Seconds to wait before timing out a connection request.
@@ -289,18 +255,18 @@ Miscellaneous Options
Read arguments from file `args.txt`, a shortcut to provide arguments from 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 file rather than command-line. This provides a convenient way to hide login
info from CLI, and can also be used to simplify managment of long arguments. info from CLI, and can also be used to simplify management of long arguments.
.. note:: .. note::
Text file should separate arguments with line breaks. Text file should separate arguments with line breaks.
args.txt example:: args.txt example::
--login=MYUSERNAME --login=MYUSERNAME
--password=MYPASSWORD --password=MYPASSWORD
--fast-update --fast-update
profile1 profile1
profile2 profile2
.. versionadded:: 4.1 .. versionadded:: 4.1
+8 -33
View File
@@ -42,7 +42,7 @@ The code example with :func:`~itertools.dropwhile` and
:func:`~itertools.takewhile` makes the assumption that the post iterator returns :func:`~itertools.takewhile` makes the assumption that the post iterator returns
posts in exact chronological order. As discussed in :issue:`666`, the following 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 approach fits for an **almost chronological order**, where up to *k* older posts
are inserted into an otherwise chronological order, such as an Hashtag feed. are inserted into an otherwise chronological order, such as a Hashtag feed.
.. literalinclude:: codesnippets/666_historical_hashtag_data.py .. literalinclude:: codesnippets/666_historical_hashtag_data.py
@@ -50,7 +50,7 @@ Likes of a Profile / Ghost Followers
------------------------------------ ------------------------------------
To obtain a list of your inactive followers, i.e. followers that did not like 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 .. literalinclude:: codesnippets/120_ghost_followers.py
@@ -78,11 +78,11 @@ Discussed in :issue:`56`.
Only one Post per User Only one Post per User
---------------------- ----------------------
To download only the one most recent post from each user, this snippet creates a To download only the single most recent post per user within a hashtag feed,
:class:`set` that contains the users of which a post has already been this snippet uses a :class:`set` that contains the users of whom a post has
downloaded. While iterating the posts, it checks whether the post's owner already been downloaded. For each post, it checks whether the post's creator is
already is in the set. If not, the post is downloaded from Instagram and the already contained in that set. If not, the post is downloaded from Instagram and
user is added to that set. the user is added to that set.
.. literalinclude:: codesnippets/113_only_one_per_user.py .. literalinclude:: codesnippets/113_only_one_per_user.py
@@ -94,37 +94,12 @@ Discussed in :issue:`113`.
Top X Posts of User Top X Posts of User
------------------- -------------------
With Instaloader, it is easy to download the few most-liked pictres of a user. With Instaloader, it is easy to download the few most-liked pictures of a user.
.. literalinclude:: codesnippets/194_top_x_of_user.py .. literalinclude:: codesnippets/194_top_x_of_user.py
Discussed in :issue:`194`. Discussed in :issue:`194`.
Upgrade Images by Local Copies
------------------------------
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.
`updgrade-instaloader-images.py <https://gist.github.com/pavelkryukov/15f93d19a99428a284a8bcec27e0187b>`__ (external link to GitHub Gist)
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`.
Metadata JSON Files 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')
+6 -6
View File
@@ -2,26 +2,26 @@ import instaloader
L = instaloader.Instaloader() L = instaloader.Instaloader()
USER = 'your_account' USER = "your_account"
PROFILE = USER PROFILE = USER
# Your preferred way of logging in: # Load session previously saved with `instaloader -l USERNAME`:
L.load_session_from_file(USER) L.load_session_from_file(USER)
profile = instaloader.Profile.from_username(L.context, PROFILE) profile = instaloader.Profile.from_username(L.context, PROFILE)
likes = set() 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(): for post in profile.get_posts():
print(post) print(post)
likes = likes | set(post.get_likes()) 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()) followers = set(profile.get_followers())
ghosts = followers - likes ghosts = followers - likes
print('Storing ghosts into file.') print("Storing ghosts into file.")
with open('/YOUR PATH/inactive-users.txt', 'w') as f: with open("inactive-users.txt", 'w') as f:
for ghost in ghosts: for ghost in ghosts:
print(ghost.username, file=f) print(ghost.username, file=f)
@@ -9,7 +9,7 @@ SINCE = datetime(2020, 5, 10) # further from today, inclusive
UNTIL = datetime(2020, 5, 11) # closer to today, not inclusive UNTIL = datetime(2020, 5, 11) # closer to today, not inclusive
k = 0 # initiate k k = 0 # initiate k
k_list = [] # uncomment this to tune k #k_list = [] # uncomment this to tune k
for post in posts: for post in posts:
postdate = post.date postdate = post.date
@@ -24,7 +24,8 @@ for post in posts:
continue continue
else: else:
L.download_post(post, "#urbanphotography") L.download_post(post, "#urbanphotography")
k = 0 # set k to 0
# if you want to tune k, uncomment below to get your k max # if you want to tune k, uncomment below to get your k max
#k_list.append(k) #k_list.append(k)
k = 0 # set k to 0
#max(k_list) #max(k_list)
+1 -1
View File
@@ -374,6 +374,6 @@ texinfo_documents = [
def setup(app): def setup(app):
typing.TYPE_CHECKING = True typing.TYPE_CHECKING = True
app.add_stylesheet("instaloader.css") app.add_stylesheet("instaloaderdoc.css")
app.connect('autodoc-process-signature', sphinx_autodoc_typehints.process_signature) app.connect('autodoc-process-signature', sphinx_autodoc_typehints.process_signature)
app.connect('autodoc-process-docstring', sphinx_autodoc_typehints.process_docstring) app.connect('autodoc-process-docstring', sphinx_autodoc_typehints.process_docstring)
+1 -1
View File
@@ -32,7 +32,7 @@ reporting a problem, please keep the following in mind:
- Include all **error messages and tracebacks** in the report. - 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. instead of what actually happened.
- If we have closed an issue apparently inadvertently or inappropriately, please - If we have closed an issue apparently inadvertently or inappropriately, please
+7 -7
View File
@@ -5,27 +5,27 @@ Install Instaloader
.. highlight:: none .. highlight:: none
**To install Instaloader**, To **install Instaloader**,
#. Ensure that you have `Python <https://www.python.org/>`__, at least #. Ensure that you have `Python <https://www.python.org/>`__, at least
version 3.5 and `pip <https://pypi.python.org/pypi/pip>`__ version 3.5, and `pip <https://pypi.python.org/pypi/pip>`__
installed installed.
#. Then, install Instaloader using:: #. Then, install Instaloader using::
pip3 install instaloader pip3 install instaloader
**To upgrade Instaloader**, do:: To **upgrade Instaloader** to its current version, do::
pip3 install --upgrade instaloader 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, - If you do not want to use pip, even though it is highly recommended,
and prefer to **install Instaloader manually**, and prefer to **install Instaloader manually**,
`Download the Source <https://github.com/instaloader/instaloader/releases/latest>`__, `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 - On **Arch Linux**, you may install Instaloader using the
`Instaloader AUR package <https://aur.archlinux.org/packages/instaloader/>`__. `Instaloader AUR package <https://aur.archlinux.org/packages/instaloader/>`__.
@@ -33,6 +33,6 @@ Install Instaloader
- On **Windows 10**, you may download the standalone executable from the - On **Windows 10**, you may download the standalone executable from the
`current release page <https://github.com/instaloader/instaloader/releases/latest>`__. `current release page <https://github.com/instaloader/instaloader/releases/latest>`__.
- To test the most current pre-release or development version of Instaloader:: - To test the most current **pre-release** version of Instaloader::
pip3 install --pre 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 -1
View File
@@ -1,2 +1,2 @@
requests requests
sphinx!=1.7.7 sphinx~=2.3
+14 -14
View File
@@ -5,8 +5,8 @@ Troubleshooting
.. highlight:: python .. highlight:: python
429 - Too Many Requests 429 Too Many Requests
----------------------- ---------------------
Instaloader has a logic to keep track of its requests to Instagram and to obey Instaloader has a logic to keep track of its requests to Instagram and to obey
their rate limits. Since they are nowhere documented, we try them out their rate limits. Since they are nowhere documented, we try them out
@@ -14,21 +14,21 @@ experimentally. We have a daily cron job running to confirm that Instaloader
still stays within the rate limits. Nevertheless, the rate control logic assumes still stays within the rate limits. Nevertheless, the rate control logic assumes
that that
- at one time, Instaloader is the only application that consumes requests. I.e. - at one time, Instaloader is the only application that consumes requests, i.e.
neither the Instagram browser interface, nor a mobile app, nor another neither the Instagram browser interface, nor a mobile app, nor another
Instaloader instance is running in parallel, Instaloader instance is running in parallel, and
- no requests had been consumed when Instaloader starts. - no requests had been consumed when Instaloader starts.
The latter one implies that restarting or reinstantiating Instaloader often The latter one implies that restarting or reinstantiating Instaloader often
within short time is prone to cause a 429. When a request is denied with a 429, within short time is prone to cause a 429. If a request is denied with a 429,
Instaloader retries the request as soon as the temporary ban is assumed to be 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 expired. In case the retry continuously fails for some reason, which should not
happen in normal conditions, consider adjusting the happen under normal conditions, consider adjusting the
:option:`--max-connection-attempts` option. :option:`--max-connection-attempts` option.
There have been observations that services, that in their nature offer There have been observations that services, that in their nature offer
promiscious IP addresses, such as cloud, VPN and public proxy services, might be promiscuous IP addresses, such as cloud, VPN and public proxy services, might be
subject to significantly stricter limits for anonymous access. However, subject to significantly stricter limits for anonymous access. However,
logged-in accesses (see :option:`--login`) do not seem to be affected. logged-in accesses (see :option:`--login`) do not seem to be affected.
@@ -46,7 +46,7 @@ Private but not followed
You have to follow a private account to access most of its associated You have to follow a private account to access most of its associated
information. information.
Login Error Login error
----------- -----------
Instaloader's login *should* work fine, both with and without Instaloader's login *should* work fine, both with and without
@@ -55,13 +55,13 @@ issued when Instagram suspects authentication activity on your account, by
pointing the user to an URL to be opened in a browser. pointing the user to an URL to be opened in a browser.
Nevertheless, in :issue:`92` and :issue:`615` users reported problems with Nevertheless, in :issue:`92` and :issue:`615` users reported problems with
logging in. We recommend to always keep the sessionfile which Instaloader logging in. We recommend to always keep the session file which Instaloader
creates when using :option:`--login`. If a sessionfile is present, creates when using :option:`--login`. If a session file is present,
:option:`--login` does not make make use of the failure-prone login procedure. :option:`--login` does not make make use of the failure-prone login procedure.
Also, sessionfiles usually do not expire and can be copied between different Also, session files usually do not expire and can be copied between different
computers without any problems. computers without any problems.
If you do not have a sessionfile present, you may use the following script 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 (:example:`615_import_firefox_session.py`) to workaround login problems by
importing the session cookies from Firefox and bypassing Instaloader's login and importing the session cookies from Firefox and bypassing Instaloader's login and
so still use Instaloader's logged-in functionality. so still use Instaloader's logged-in functionality.
@@ -78,7 +78,7 @@ To use this script,
#. Then, ``instaloader -l USERNAME`` should work fine. #. Then, ``instaloader -l USERNAME`` should work fine.
This script also supports specifying a cookiefile path, which may be useful if 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 you use multiple Firefox profiles or if your operating system has the directory
structure differently set up. Also, you can specify an alternative sessionfile structure differently set up. Also, you can specify an alternative session file
path. path.
+1 -1
View File
@@ -1,7 +1,7 @@
"""Download pictures (or videos) along with their captions and other metadata from Instagram.""" """Download pictures (or videos) along with their captions and other metadata from Instagram."""
__version__ = '4.5a1' __version__ = '4.5.3'
try: try:
+38 -32
View File
@@ -13,7 +13,7 @@ from functools import wraps
from hashlib import md5 from hashlib import md5
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Iterator, List, Optional, Set, Union from typing import Any, Callable, IO, Iterator, List, Optional, Set, Union, cast
import requests import requests
import urllib3 # type: ignore import urllib3 # type: ignore
@@ -351,7 +351,8 @@ class Instaloader:
except UnicodeEncodeError: except UnicodeEncodeError:
self.context.log('txt', end=' ', flush=True) self.context.log('txt', end=' ', flush=True)
with open(filename, 'wb') as text_file: with open(filename, 'wb') as text_file:
shutil.copyfileobj(BytesIO(bcaption), text_file) with BytesIO(bcaption) as bio:
shutil.copyfileobj(cast(IO, bio), text_file)
os.utime(filename, (datetime.now().timestamp(), mtime.timestamp())) os.utime(filename, (datetime.now().timestamp(), mtime.timestamp()))
def save_location(self, filename: str, location: PostLocation, mtime: datetime) -> None: def save_location(self, filename: str, location: PostLocation, mtime: datetime) -> None:
@@ -361,7 +362,8 @@ class Instaloader:
"https://maps.google.com/maps?q={0},{1}&ll={0},{1}\n".format(location.lat, "https://maps.google.com/maps?q={0},{1}&ll={0},{1}\n".format(location.lat,
location.lng)) location.lng))
with open(filename, 'wb') as text_file: with open(filename, 'wb') as text_file:
shutil.copyfileobj(BytesIO(location_string.encode()), text_file) with BytesIO(location_string.encode()) as bio:
shutil.copyfileobj(cast(IO, bio), text_file)
os.utime(filename, (datetime.now().timestamp(), mtime.timestamp())) os.utime(filename, (datetime.now().timestamp(), mtime.timestamp()))
self.context.log('geo', end=' ', flush=True) self.context.log('geo', end=' ', flush=True)
@@ -511,26 +513,28 @@ class Instaloader:
# Download the image(s) / video thumbnail and videos within sidecars if desired # Download the image(s) / video thumbnail and videos within sidecars if desired
downloaded = True downloaded = True
if self.download_pictures: if post.typename == 'GraphSidecar':
if post.typename == 'GraphSidecar': if self.download_pictures or self.download_videos:
edge_number = 1 for edge_number, sidecar_node in enumerate(post.get_sidecar_nodes(), start=1):
for sidecar_node in post.get_sidecar_nodes(): if self.download_pictures and (not sidecar_node.is_video or self.download_video_thumbnails):
# Download picture or video thumbnail # Download sidecar picture or video thumbnail (--no-pictures implies --no-video-thumbnails)
if not sidecar_node.is_video or self.download_video_thumbnails is True:
downloaded &= self.download_pic(filename=filename, url=sidecar_node.display_url, downloaded &= self.download_pic(filename=filename, url=sidecar_node.display_url,
mtime=post.date_local, filename_suffix=str(edge_number)) mtime=post.date_local, filename_suffix=str(edge_number))
# Additionally download video if available and desired if sidecar_node.is_video and self.download_videos:
if sidecar_node.is_video and self.download_videos is True: # Download sidecar video if desired
downloaded &= self.download_pic(filename=filename, url=sidecar_node.video_url, downloaded &= self.download_pic(filename=filename, url=sidecar_node.video_url,
mtime=post.date_local, filename_suffix=str(edge_number)) mtime=post.date_local, filename_suffix=str(edge_number))
edge_number += 1 elif post.typename == 'GraphImage':
elif post.typename == 'GraphImage': # Download picture
if self.download_pictures:
downloaded = self.download_pic(filename=filename, url=post.url, mtime=post.date_local) downloaded = self.download_pic(filename=filename, url=post.url, mtime=post.date_local)
elif post.typename == 'GraphVideo': elif post.typename == 'GraphVideo':
if self.download_video_thumbnails is True: # Download video thumbnail (--no-pictures implies --no-video-thumbnails)
if self.download_pictures and self.download_video_thumbnails:
with self.context.error_catcher("Video thumbnail of {}".format(post)):
downloaded = self.download_pic(filename=filename, url=post.url, mtime=post.date_local) downloaded = self.download_pic(filename=filename, url=post.url, mtime=post.date_local)
else: else:
self.context.error("Warning: {0} has unknown typename: {1}".format(post, post.typename)) self.context.error("Warning: {0} has unknown typename: {1}".format(post, post.typename))
# Save caption if desired # Save caption if desired
metadata_string = _ArbitraryItemFormatter(post).format(self.post_metadata_txt_pattern).strip() metadata_string = _ArbitraryItemFormatter(post).format(self.post_metadata_txt_pattern).strip()
@@ -538,7 +542,7 @@ class Instaloader:
self.save_caption(filename=filename, mtime=post.date_local, caption=metadata_string) self.save_caption(filename=filename, mtime=post.date_local, caption=metadata_string)
# Download video if desired # Download video if desired
if post.is_video and self.download_videos is True: if post.is_video and self.download_videos:
downloaded &= self.download_pic(filename=filename, url=post.video_url, mtime=post.date_local) downloaded &= self.download_pic(filename=filename, url=post.video_url, mtime=post.date_local)
# Download geotags if desired # Download geotags if desired
@@ -546,11 +550,11 @@ class Instaloader:
self.save_location(filename, post.location, post.date_local) self.save_location(filename, post.location, post.date_local)
# Update comments if desired # Update comments if desired
if self.download_comments is True: if self.download_comments:
self.update_comments(filename=filename, post=post) self.update_comments(filename=filename, post=post)
# Save metadata as JSON if desired. # Save metadata as JSON if desired.
if self.save_metadata is not False: if self.save_metadata:
self.save_metadata_json(filename, post) self.save_metadata_json(filename, post)
self.context.log() self.context.log()
@@ -728,9 +732,9 @@ class Instaloader:
""" """
Download the Posts returned by given Post Iterator. Download the Posts returned by given Post Iterator.
..versionadded:: 4.4 .. versionadded:: 4.4
..versionchanged:: 4.5 .. versionchanged:: 4.5
Transparently resume an aborted operation if `posts` is a :class:`NodeIterator`. Transparently resume an aborted operation if `posts` is a :class:`NodeIterator`.
:param posts: Post Iterator to loop through. :param posts: Post Iterator to loop through.
@@ -743,27 +747,29 @@ class Instaloader:
""" """
displayed_count = (max_count if total_count is None or max_count is not None and max_count < total_count displayed_count = (max_count if total_count is None or max_count is not None and max_count < total_count
else total_count) else total_count)
sanitized_target = target
if isinstance(target, str):
sanitized_target = _PostPathFormatter.sanitize_path(target)
with resumable_iteration( with resumable_iteration(
context=self.context, context=self.context,
iterator=posts, iterator=posts,
load=load_structure_from_file, load=load_structure_from_file,
save=save_structure_to_file, save=save_structure_to_file,
format_path=lambda magic: self.format_filename_within_target_path( format_path=lambda magic: self.format_filename_within_target_path(
target, owner_profile, self.resume_prefix or '', magic, 'json.xz' sanitized_target, owner_profile, self.resume_prefix or '', magic, 'json.xz'
), ),
check_bbd=self.check_resume_bbd, check_bbd=self.check_resume_bbd,
enabled=self.resume_prefix is not None enabled=self.resume_prefix is not None
) as resume_info: ) as (is_resuming, start_index):
is_resuming, start_index = resume_info for number, post in enumerate(posts, start=start_index + 1):
for number, post in enumerate(posts): if max_count is not None and number > max_count:
if max_count is not None and number + start_index >= max_count:
break break
if displayed_count is not None: if displayed_count is not None:
self.context.log("[{0:{w}d}/{1:{w}d}] ".format(number + start_index + 1, displayed_count, self.context.log("[{0:{w}d}/{1:{w}d}] ".format(number, displayed_count,
w=len(str(displayed_count))), w=len(str(displayed_count))),
end="", flush=True) end="", flush=True)
else: else:
self.context.log("[{:3d}] ".format(number + start_index + 1), end="", flush=True) self.context.log("[{:3d}] ".format(number), end="", flush=True)
if post_filter is not None: if post_filter is not None:
try: try:
if not post_filter(post): if not post_filter(post):
@@ -851,7 +857,7 @@ class Instaloader:
""" """
self.context.log("Retrieving saved posts...") self.context.log("Retrieving saved posts...")
assert self.context.username is not None # safe due to @_requires_login; required by typechecker assert self.context.username is not None # safe due to @_requires_login; required by typechecker
node_iterator = Profile.from_username(self.context, self.context.username).get_saved_posts() node_iterator = Profile.own_profile(self.context).get_saved_posts()
self.posts_download_loop(node_iterator, ":saved", self.posts_download_loop(node_iterator, ":saved",
fast_update, post_filter, fast_update, post_filter,
max_count=max_count, total_count=node_iterator.count) max_count=max_count, total_count=node_iterator.count)
@@ -1103,11 +1109,11 @@ class Instaloader:
def _error_raiser(_str): def _error_raiser(_str):
yield yield
# error_handler type is Callable[[Optional[str]], ContextManager[None]] (not supported with Python 3.5) # error_handler type is Callable[[Optional[str]], ContextManager[None]] (not supported with Python 3.5.0..3.5.3)
error_handler = _error_raiser if raise_errors else self.context.error_catcher error_handler = _error_raiser if raise_errors else self.context.error_catcher
for profile in profiles: for profile in profiles:
with error_handler(profile.username): # type: ignore with error_handler(profile.username): # type: ignore # (ignore type for Python 3.5 support)
profile_name = profile.username profile_name = profile.username
# Download profile picture # Download profile picture
+82 -44
View File
@@ -24,7 +24,7 @@ def copy_session(session: requests.Session, request_timeout: Optional[float] = N
"""Duplicates a requests.Session.""" """Duplicates a requests.Session."""
new = requests.Session() new = requests.Session()
new.cookies = requests.utils.cookiejar_from_dict(requests.utils.dict_from_cookiejar(session.cookies)) new.cookies = requests.utils.cookiejar_from_dict(requests.utils.dict_from_cookiejar(session.cookies))
new.headers = session.headers.copy() # type: ignore new.headers = session.headers.copy()
if request_timeout is not None: if request_timeout is not None:
# Override default timeout behavior. # Override default timeout behavior.
# Need to silence mypy bug for this. See: https://github.com/python/mypy/issues/2427 # Need to silence mypy bug for this. See: https://github.com/python/mypy/issues/2427
@@ -351,10 +351,10 @@ class InstaloaderContext:
raise ConnectionException("\"window._sharedData\" does not contain required keys.") raise ConnectionException("\"window._sharedData\" does not contain required keys.")
# If GraphQL data is missing in `window._sharedData`, search for it in `__additionalDataLoaded`. # If GraphQL data is missing in `window._sharedData`, search for it in `__additionalDataLoaded`.
if 'graphql' not in post_or_profile_page[0]: if 'graphql' not in post_or_profile_page[0]:
match = re.search(r'window\.__additionalDataLoaded\([^{]+{"graphql":({.*})}\);</script>', match = re.search(r'window\.__additionalDataLoaded\(.*?({.*"graphql":.*})\);</script>',
resp.text) resp.text)
if match is not None: if match is not None:
post_or_profile_page[0]['graphql'] = json.loads(match.group(1)) post_or_profile_page[0]['graphql'] = json.loads(match.group(1))['graphql']
return resp_json return resp_json
else: else:
resp_json = resp.json() resp_json = resp.json()
@@ -431,7 +431,7 @@ class InstaloaderContext:
""" """
Retrieve a list of GraphQL nodes. Retrieve a list of GraphQL nodes.
..deprecated:: 4.5 .. deprecated:: 4.5
Use :class:`NodeIterator` instead, which provides more functionality. Use :class:`NodeIterator` instead, which provides more functionality.
""" """
@@ -487,7 +487,7 @@ class InstaloaderContext:
shutil.copyfileobj(resp.raw, file) shutil.copyfileobj(resp.raw, file)
else: else:
file.write(resp) file.write(resp)
os.rename(filename + '.temp', filename) os.replace(filename + '.temp', filename)
def get_raw(self, url: str, _attempt=1) -> requests.Response: def get_raw(self, url: str, _attempt=1) -> requests.Response:
"""Downloads a file anonymously. """Downloads a file anonymously.
@@ -549,8 +549,8 @@ class RateController:
def __init__(self, context: InstaloaderContext): def __init__(self, context: InstaloaderContext):
self._context = context self._context = context
self._graphql_query_timestamps = dict() # type: Dict[str, List[float]] self._query_timestamps = dict() # type: Dict[str, List[float]]
self._graphql_earliest_next_request_time = 0.0 self._earliest_next_request_time = 0.0
def sleep(self, secs: float): def sleep(self, secs: float):
"""Wait given number of seconds.""" """Wait given number of seconds."""
@@ -559,38 +559,75 @@ class RateController:
# pylint:disable=no-self-use # pylint:disable=no-self-use
time.sleep(secs) time.sleep(secs)
def _dump_query_timestamps(self, current_time: float): def _dump_query_timestamps(self, current_time: float, failed_query_type: str):
windows = [10, 11, 15, 20, 30, 60] windows = [10, 11, 20, 22, 30, 60]
print("GraphQL requests:", file=sys.stderr) self._context.error("Requests within last {} minutes grouped by type:"
for query_hash, times in self._graphql_query_timestamps.items(): .format('/'.join(str(w) for w in windows)),
print(" {}".format(query_hash), file=sys.stderr) repeat_at_end=False)
for window in windows: for query_type, times in self._query_timestamps.items():
reqs_in_sliding_window = sum(t > current_time - window * 60 for t in times) reqs_in_sliding_window = [sum(t > current_time - w * 60 for t in times) for w in windows]
print(" last {} minutes: {} requests".format(window, reqs_in_sliding_window), file=sys.stderr) 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: def count_per_sliding_window(self, query_type: str) -> int:
"""Return how many GraphQL requests can be done within the sliding window.""" """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 # Not static, to allow for the count_per_sliding_window to depend on context-inherent properties, such as
# whether we are logged in. # whether we are logged in.
# pylint:disable=no-self-use,unused-argument # pylint:disable=no-self-use
return 200 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: def query_waittime(self, query_type: str, current_time: float, untracked_queries: bool = False) -> float:
"""Calculate time needed to wait before GraphQL query can be executed.""" """Calculate time needed to wait before query can be executed."""
sliding_window = 660 per_type_sliding_window = 660
if query_type not in self._graphql_query_timestamps: if query_type not in self._query_timestamps:
self._graphql_query_timestamps[query_type] = [] self._query_timestamps[query_type] = []
self._graphql_query_timestamps[query_type] = list(filter(lambda t: t > current_time - 60 * 60, self._query_timestamps[query_type] = list(filter(lambda t: t > current_time - 60 * 60,
self._graphql_query_timestamps[query_type])) self._query_timestamps[query_type]))
reqs_in_sliding_window = list(filter(lambda t: t > current_time - sliding_window,
self._graphql_query_timestamps[query_type])) def per_type_next_request_time():
count_per_sliding_window = self.count_per_sliding_window(query_type) reqs_in_sliding_window = self._reqs_in_sliding_window(query_type, current_time, per_type_sliding_window)
if len(reqs_in_sliding_window) < count_per_sliding_window and not untracked_queries: if len(reqs_in_sliding_window) < self.count_per_sliding_window(query_type):
return max(0.0, self._graphql_earliest_next_request_time - current_time) return 0.0
next_request_time = min(reqs_in_sliding_window) + sliding_window + 6 else:
if untracked_queries: return min(reqs_in_sliding_window) + per_type_sliding_window + 6
self._graphql_earliest_next_request_time = next_request_time
return max(next_request_time, self._graphql_earliest_next_request_time) - current_time 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: 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 """This method is called before a query to Instagram. It calls :meth:`RateController.sleep` to wait
@@ -602,24 +639,25 @@ class RateController:
.format(round(waittime), datetime.now() + timedelta(seconds=waittime))) .format(round(waittime), datetime.now() + timedelta(seconds=waittime)))
if waittime > 0: if waittime > 0:
self.sleep(waittime) self.sleep(waittime)
if query_type not in self._graphql_query_timestamps: if query_type not in self._query_timestamps:
self._graphql_query_timestamps[query_type] = [time.monotonic()] self._query_timestamps[query_type] = [time.monotonic()]
else: else:
self._graphql_query_timestamps[query_type].append(time.monotonic()) self._query_timestamps[query_type].append(time.monotonic())
def handle_429(self, query_type: str) -> None: 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 """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.""" wait until we can repeat the same request."""
text_for_429 = ("HTTP error code 429 was returned because too many queries occurred in the last time. "
"Please do not use Instagram in your browser or run multiple instances of Instaloader "
"in parallel.")
print(textwrap.fill(text_for_429), file=sys.stderr)
current_time = time.monotonic() current_time = time.monotonic()
waittime = self.query_waittime(query_type, current_time, True) waittime = self.query_waittime(query_type, current_time, True)
assert waittime >= 0 assert waittime >= 0
if waittime > 15: self._dump_query_timestamps(current_time, query_type)
self._context.log("The request will be retried in {} seconds, at {:%H:%M}." text_for_429 = ("Instagram responded with HTTP error \"429 - Too Many Requests\". Please do not run multiple "
.format(round(waittime), datetime.now() + timedelta(seconds=waittime))) "instances of Instaloader in parallel or within short sequence. Also, do not use any Instagram "
self._dump_query_timestamps(current_time) "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: if waittime > 0:
self.sleep(waittime) self.sleep(waittime)
+24 -13
View File
@@ -59,13 +59,13 @@ class NodeIterator(Iterator[T]):
been constructed with the same parameters as the instance that is represented by the :class:`FrozenNodeIterator` in 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 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 distinguish iterators that are saved e.g. in files, there is the :attr:`NodeIterator.magic` string: Two
NodeIterators are matching if they have the same magic. 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. See also :func:`resumable_iteration` for a high-level context manager that handles a resumable iteration.
""" """
_graphql_page_length = 50 _graphql_page_length = 50
shelf_life = timedelta(days=29) _shelf_life = timedelta(days=29)
def __init__(self, def __init__(self,
context: InstaloaderContext, context: InstaloaderContext,
@@ -81,11 +81,13 @@ class NodeIterator(Iterator[T]):
self._node_wrapper = node_wrapper self._node_wrapper = node_wrapper
self._query_variables = query_variables if query_variables is not None else {} self._query_variables = query_variables if query_variables is not None else {}
self._query_referer = query_referer self._query_referer = query_referer
self._data = first_data
self._page_index = 0 self._page_index = 0
self._total_index = 0 self._total_index = 0
self._best_before = (None if first_data is None else if first_data is not None:
datetime.now() + NodeIterator.shelf_life) 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: def _query(self, after: Optional[str] = None) -> Dict:
pagination_variables = {'first': NodeIterator._graphql_page_length} # type: Dict[str, Any] pagination_variables = {'first': NodeIterator._graphql_page_length} # type: Dict[str, Any]
@@ -97,7 +99,7 @@ class NodeIterator(Iterator[T]):
self._query_hash, {**self._query_variables, **pagination_variables}, self._query_referer self._query_hash, {**self._query_variables, **pagination_variables}, self._query_referer
) )
) )
self._best_before = datetime.now() + NodeIterator.shelf_life self._best_before = datetime.now() + NodeIterator._shelf_life
return data return data
except QueryReturnedBadRequestException: except QueryReturnedBadRequestException:
new_page_length = int(NodeIterator._graphql_page_length / 2) new_page_length = int(NodeIterator._graphql_page_length / 2)
@@ -112,9 +114,7 @@ class NodeIterator(Iterator[T]):
def __iter__(self): def __iter__(self):
return self return self
def __next__(self): def __next__(self) -> T:
if self._data is None:
self._data = self._query()
if self._page_index < len(self._data['edges']): if self._page_index < len(self._data['edges']):
node = self._data['edges'][self._page_index]['node'] node = self._data['edges'][self._page_index]['node']
page_index, total_index = self._page_index, self._total_index page_index, total_index = self._page_index, self._total_index
@@ -177,7 +177,15 @@ class NodeIterator(Iterator[T]):
) )
def thaw(self, frozen: FrozenNodeIterator) -> None: def thaw(self, frozen: FrozenNodeIterator) -> None:
"""Use this iterator for resuming from earlier iteration.""" """
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: if self._total_index or self._page_index:
raise InvalidArgumentException("thaw() called on already-used iterator.") raise InvalidArgumentException("thaw() called on already-used iterator.")
if (self._query_hash != frozen.query_hash or if (self._query_hash != frozen.query_hash or
@@ -185,8 +193,12 @@ class NodeIterator(Iterator[T]):
self._query_referer != frozen.query_referer or self._query_referer != frozen.query_referer or
self._context.username != frozen.context_username): self._context.username != frozen.context_username):
raise InvalidArgumentException("Mismatching resume information.") 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._total_index = frozen.total_index
self._best_before = datetime.fromtimestamp(frozen.best_before) if frozen.best_before else None self._best_before = datetime.fromtimestamp(frozen.best_before)
self._data = frozen.remaining_data self._data = frozen.remaining_data
@@ -211,8 +223,7 @@ def resumable_iteration(context: InstaloaderContext,
load=lambda _, path: FrozenNodeIterator(**json.load(open(path))), load=lambda _, path: FrozenNodeIterator(**json.load(open(path))),
save=lambda fni, path: json.dump(fni._asdict(), open(path, 'w')), save=lambda fni, path: json.dump(fni._asdict(), open(path, 'w')),
format_path=lambda magic: "resume_info_{}.json".format(magic) format_path=lambda magic: "resume_info_{}.json".format(magic)
) as resume_info: ) as (is_resuming, start_index):
is_resuming, start_index = resume_info
for post in post_iterator: for post in post_iterator:
do_something_with(post) do_something_with(post)
+18 -9
View File
@@ -26,7 +26,7 @@ PostCommentAnswer.likes_count.__doc__ = "Number of likes on comment."
PostComment = namedtuple('PostComment', (*PostCommentAnswer._fields, 'answers')) # type: ignore PostComment = namedtuple('PostComment', (*PostCommentAnswer._fields, 'answers')) # type: ignore
for field in PostCommentAnswer._fields: for field in PostCommentAnswer._fields:
getattr(PostComment, field).__doc__ = getattr(PostCommentAnswer, field).__doc__ getattr(PostComment, field).__doc__ = getattr(PostCommentAnswer, field).__doc__ # pylint: disable=no-member
PostComment.answers.__doc__ = r"Iterator which yields all :class:`PostCommentAnswer`\ s for the comment." # type: ignore PostComment.answers.__doc__ = r"Iterator which yields all :class:`PostCommentAnswer`\ s for the comment." # type: ignore
PostLocation = namedtuple('PostLocation', ['id', 'name', 'slug', 'has_public_page', 'lat', 'lng']) PostLocation = namedtuple('PostLocation', ['id', 'name', 'slug', 'has_public_page', 'lat', 'lng'])
@@ -549,7 +549,7 @@ class Profile:
""" """
# pylint:disable=protected-access # pylint:disable=protected-access
profile = cls(context, {'username': username.lower()}) profile = cls(context, {'username': username.lower()})
profile._obtain_metadata() # to raise ProfileNotExistException now in case username is invalid profile._obtain_metadata() # to raise ProfileNotExistsException now in case username is invalid
return profile return profile
@classmethod @classmethod
@@ -579,6 +579,17 @@ class Profile:
context.profile_id_cache[profile_id] = profile context.profile_id_cache[profile_id] = profile
return profile return profile
@classmethod
def own_profile(cls, context: InstaloaderContext):
"""Return own profile if logged-in.
:param context: :attr:`Instaloader.context`
.. versionadded:: 4.5.2"""
if not context.is_logged_in:
raise LoginRequiredException("--login required to access own profile.")
return cls(context, context.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {})["data"]["user"])
def _asdict(self): def _asdict(self):
json_node = self._node.copy() json_node = self._node.copy()
# remove posts to avoid "Circular reference detected" exception # remove posts to avoid "Circular reference detected" exception
@@ -782,7 +793,7 @@ class Profile:
def get_posts(self) -> NodeIterator[Post]: def get_posts(self) -> NodeIterator[Post]:
"""Retrieve all posts from a profile. """Retrieve all posts from a profile.
:rtype:NodeIterator[Post]""" :rtype: NodeIterator[Post]"""
self._obtain_metadata() self._obtain_metadata()
return NodeIterator( return NodeIterator(
self._context, self._context,
@@ -797,12 +808,11 @@ class Profile:
def get_saved_posts(self) -> NodeIterator[Post]: def get_saved_posts(self) -> NodeIterator[Post]:
"""Get Posts that are marked as saved by the user. """Get Posts that are marked as saved by the user.
:rtype:NodeIterator[Post]""" :rtype: NodeIterator[Post]"""
if self.username != self._context.username: if self.username != self._context.username:
raise LoginRequiredException("--login={} required to get that profile's saved posts.".format(self.username)) raise LoginRequiredException("--login={} required to get that profile's saved posts.".format(self.username))
self._obtain_metadata()
return NodeIterator( return NodeIterator(
self._context, self._context,
'f883d95537fbcd400f466f63d42bd8a1', 'f883d95537fbcd400f466f63d42bd8a1',
@@ -810,7 +820,6 @@ class Profile:
lambda n: Post(self._context, n), lambda n: Post(self._context, n),
{'id': self.userid}, {'id': self.userid},
'https://www.instagram.com/{0}/'.format(self.username), 'https://www.instagram.com/{0}/'.format(self.username),
self._metadata('edge_saved_media'),
) )
def get_tagged_posts(self) -> NodeIterator[Post]: def get_tagged_posts(self) -> NodeIterator[Post]:
@@ -851,7 +860,7 @@ class Profile:
Retrieve list of followers of given profile. Retrieve list of followers of given profile.
To use this, one needs to be logged in and private profiles has to be followed. To use this, one needs to be logged in and private profiles has to be followed.
:rtype:NodeIterator[Profile] :rtype: NodeIterator[Profile]
""" """
if not self._context.is_logged_in: if not self._context.is_logged_in:
raise LoginRequiredException("--login required to get a profile's followers.") raise LoginRequiredException("--login required to get a profile's followers.")
@@ -870,7 +879,7 @@ class Profile:
Retrieve list of followees (followings) of given profile. Retrieve list of followees (followings) of given profile.
To use this, one needs to be logged in and private profiles has to be followed. To use this, one needs to be logged in and private profiles has to be followed.
:rtype:NodeIterator[Profile] :rtype: NodeIterator[Profile]
""" """
if not self._context.is_logged_in: if not self._context.is_logged_in:
raise LoginRequiredException("--login required to get a profile's followees.") raise LoginRequiredException("--login required to get a profile's followees.")
@@ -1334,7 +1343,7 @@ class Hashtag:
yield from (Post(self._context, edge["node"]) for edge in conn["edges"]) yield from (Post(self._context, edge["node"]) for edge in conn["edges"])
def get_all_posts(self) -> Iterator[Post]: def get_all_posts(self) -> Iterator[Post]:
"""Yields all posts, i.e. all most recent posts and the top posts, in chronological order.""" """Yields all posts, i.e. all most recent posts and the top posts, in almost-chronological order."""
sorted_top_posts = iter(sorted(self.get_top_posts(), key=lambda p: p.date_utc, reverse=True)) sorted_top_posts = iter(sorted(self.get_top_posts(), key=lambda p: p.date_utc, reverse=True))
other_posts = self.get_posts() other_posts = self.get_posts()
next_top = next(sorted_top_posts, None) next_top = next(sorted_top_posts, None)