mirror of
https://github.com/instaloader/instaloader.git
synced 2026-10-19 15:12:16 -07:00
Compare commits
493
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
195569410b | ||
|
|
57393f619d | ||
|
|
d5dd8cec00 | ||
|
|
3652e009d9 | ||
|
|
ecde92d05e | ||
|
|
a8d3851956 | ||
|
|
c71873231d | ||
|
|
3823b1e593 | ||
|
|
6c779e8d4b | ||
|
|
c24015a93d | ||
|
|
2c56e6994a | ||
|
|
6791e9a2f8 | ||
|
|
64652f3994 | ||
|
|
4f5b223b60 | ||
|
|
2a3711687f | ||
|
|
612842f255 | ||
|
|
fe9cd653e7 | ||
|
|
82e7b19d6d | ||
|
|
5d7c36399f | ||
|
|
f0e66ef465 | ||
|
|
f71ea562af | ||
|
|
bc40b82f94 | ||
|
|
c817d1901a | ||
|
|
e232c82d5e | ||
|
|
601dd0b106 | ||
|
|
4c20b573e2 | ||
|
|
db63e5c7e5 | ||
|
|
fbe05a1add | ||
|
|
e21d34124d | ||
|
|
fa62025ea3 | ||
|
|
66b3ec6928 | ||
|
|
f7d82495bd | ||
|
|
b0a430051e | ||
|
|
8b56b90e72 | ||
|
|
0c93e63615 | ||
|
|
65025e61d9 | ||
|
|
11baf493db | ||
|
|
2345d90e0e | ||
|
|
71b09aed0f | ||
|
|
fd6d8542c9 | ||
|
|
df6fbbee76 | ||
|
|
1661bf7722 | ||
|
|
8f9723cf62 | ||
|
|
e1567a0342 | ||
|
|
610b6242a0 | ||
|
|
d90f90988a | ||
|
|
d3c747a306 | ||
|
|
225353dc21 | ||
|
|
ed499cb49c | ||
|
|
1c13d5a30b | ||
|
|
2fd1422619 | ||
|
|
c818165db4 | ||
|
|
26a78463f1 | ||
|
|
689dd3613b | ||
|
|
27b7c98be2 | ||
|
|
72d86533f2 | ||
|
|
7bb08a220a | ||
|
|
03aabc52bb | ||
|
|
9d4df0d5fb | ||
|
|
bf2f4f16ed | ||
|
|
1a3712d112 | ||
|
|
65462b01ec | ||
|
|
9857c5fa24 | ||
|
|
83f013f6ca | ||
|
|
e7437310ea | ||
|
|
dcba25a40e | ||
|
|
5e4b5fd877 | ||
|
|
657e76e2d9 | ||
|
|
78023b3c98 | ||
|
|
ab67ca30e5 | ||
|
|
0e13c73b62 | ||
|
|
b98e9941ae | ||
|
|
c9e4f68597 | ||
|
|
1338cf0fcd | ||
|
|
83a0f52c60 | ||
|
|
5c5c633bee | ||
|
|
e268f9e625 | ||
|
|
793b7e82ba | ||
|
|
7329ee10d8 | ||
|
|
57d2ce9538 | ||
|
|
dc458a3f3c | ||
|
|
08a359408c | ||
|
|
a794089c4f | ||
|
|
c56657855d | ||
|
|
8c7561c5d7 | ||
|
|
c76385bde2 | ||
|
|
b5bf8a0560 | ||
|
|
2790342a09 | ||
|
|
e725ca2640 | ||
|
|
9c2e7c70ac | ||
|
|
ffbf5b517c | ||
|
|
0843e10187 | ||
|
|
78a93c945e | ||
|
|
bb3e309195 | ||
|
|
6cc2ed54ea | ||
|
|
41cf260fdd | ||
|
|
f910705c51 | ||
|
|
aa3f19dcbe | ||
|
|
fe258f1abb | ||
|
|
534497bea5 | ||
|
|
819426efdb | ||
|
|
0aac91680f | ||
|
|
dc289024c8 | ||
|
|
b2acbcf0e0 | ||
|
|
de585becdb | ||
|
|
f966ea5bb9 | ||
|
|
302145f54f | ||
|
|
8764947d10 | ||
|
|
d3403bc686 | ||
|
|
b7bae19374 | ||
|
|
4e87a8cf63 | ||
|
|
fd13d66367 | ||
|
|
63eb9e71ca | ||
|
|
d8ba30b91b | ||
|
|
ec0e017b10 | ||
|
|
51bfa01c2a | ||
|
|
e8d4054f84 | ||
|
|
cf293ce312 | ||
|
|
3226c43a98 | ||
|
|
20819d2597 | ||
|
|
4e8236b6e9 | ||
|
|
b542936c09 | ||
|
|
af4dbc00d7 | ||
|
|
0aa1ec7c76 | ||
|
|
75a729781d | ||
|
|
a88316224b | ||
|
|
87dc82d1b1 | ||
|
|
6a3b05762e | ||
|
|
6d5bb1e181 | ||
|
|
819057eb6f | ||
|
|
5235cc0f6a | ||
|
|
fb06b4b7d6 | ||
|
|
1a382cd61c | ||
|
|
0345da71ec | ||
|
|
749ff60639 | ||
|
|
3528e8d19b | ||
|
|
5fce6946ea | ||
|
|
692cbc000d | ||
|
|
102f27c4ef | ||
|
|
33d13c4b32 | ||
|
|
408cf22c18 | ||
|
|
93882d93e0 | ||
|
|
8ef7b3242c | ||
|
|
406f33896a | ||
|
|
b9be1eaaf4 | ||
|
|
6b24b54591 | ||
|
|
67d3b4fca1 | ||
|
|
4c72077976 | ||
|
|
4f9d64a284 | ||
|
|
0c9a667da7 | ||
|
|
ccf55dbeef | ||
|
|
87d877e650 | ||
|
|
adb9f3a346 | ||
|
|
2aa26ea140 | ||
|
|
0fd06fe6bc | ||
|
|
15d9cd8949 | ||
|
|
9421ad5fda | ||
|
|
1e89ebe038 | ||
|
|
3f4c12b54b | ||
|
|
dcc57f11c4 | ||
|
|
a9e79e90f9 | ||
|
|
a0b7804fd2 | ||
|
|
9e04ef3436 | ||
|
|
34416298a0 | ||
|
|
e39ffecea4 | ||
|
|
15fe3d5f2d | ||
|
|
c73b10843f | ||
|
|
b158c57b9e | ||
|
|
57892f191a | ||
|
|
26e74bad6a | ||
|
|
27a239f07f | ||
|
|
bb25f03c7d | ||
|
|
c54342bb98 | ||
|
|
d2bebcd00c | ||
|
|
4bc0a94e12 | ||
|
|
06845b53fc | ||
|
|
e9cec0b261 | ||
|
|
ac4cd9f595 | ||
|
|
2e7835850a | ||
|
|
89b56bdfa8 | ||
|
|
1387d396dd | ||
|
|
4ee867c61b | ||
|
|
edba6959d9 | ||
|
|
08327c4117 | ||
|
|
4ab0fea82a | ||
|
|
a735b02bac | ||
|
|
be5d02ef3b | ||
|
|
1ab9e44104 | ||
|
|
6212d22e26 | ||
|
|
aa859b9f54 | ||
|
|
ffda2265bd | ||
|
|
e753406a6c | ||
|
|
30555b576c | ||
|
|
ece0d11915 | ||
|
|
41e734e1b8 | ||
|
|
b7ea0d5953 | ||
|
|
325e89c4b2 | ||
|
|
e248020927 | ||
|
|
5a7b76db31 | ||
|
|
1a239e17cb | ||
|
|
bb8749b753 | ||
|
|
1f311d3e1e | ||
|
|
862c51fa81 | ||
|
|
cc15cb5857 | ||
|
|
18707ee232 | ||
|
|
0e534ba519 | ||
|
|
084424e856 | ||
|
|
567a04a661 | ||
|
|
1394e8e9f5 | ||
|
|
86204d165d | ||
|
|
d0e2ac4388 | ||
|
|
60d47be2f3 | ||
|
|
f988762cb1 | ||
|
|
b443cc6654 | ||
|
|
584c69d93c | ||
|
|
cbdd85ef07 | ||
|
|
2e517e972f | ||
|
|
54572fb1fc | ||
|
|
158c1433bb | ||
|
|
43f52198ff | ||
|
|
91d5d5f867 | ||
|
|
0dcc912987 | ||
|
|
180d398ad7 | ||
|
|
f262c4347f | ||
|
|
b91e73b090 | ||
|
|
e388a1c966 | ||
|
|
0f0ac13d72 | ||
|
|
06b7edd6d5 | ||
|
|
5f57345f1b | ||
|
|
f7e6632f68 | ||
|
|
9258b84695 | ||
|
|
e00d77c234 | ||
|
|
a3777cad5d | ||
|
|
8303f1d1c5 | ||
|
|
998333b7cd | ||
|
|
77ca78e977 | ||
|
|
dcea0e9672 | ||
|
|
809b23a996 | ||
|
|
c634f21dcc | ||
|
|
a070ed42ae | ||
|
|
a34d750152 | ||
|
|
bfa5d8ed41 | ||
|
|
7f81985911 | ||
|
|
b5aa268344 | ||
|
|
92653dcdb9 | ||
|
|
1b5c12c8fa | ||
|
|
bc29ffbd44 | ||
|
|
50a5330fec | ||
|
|
1739844758 | ||
|
|
0ebb223214 | ||
|
|
3d4109ded3 | ||
|
|
8423e3bd00 | ||
|
|
5b0f8b75cc | ||
|
|
3cb33603e5 | ||
|
|
47db7f7ffa | ||
|
|
2567bec975 | ||
|
|
2df1419d68 | ||
|
|
8211c9daf1 | ||
|
|
969d4e3aa4 | ||
|
|
4cf1ae847d | ||
|
|
a276a0dc59 | ||
|
|
cc34b8340f | ||
|
|
45fe509ac4 | ||
|
|
ae492ed68b | ||
|
|
0e433294ad | ||
|
|
a807408d8f | ||
|
|
86035b8d2d | ||
|
|
a3eb93c21a | ||
|
|
b9c7713d6a | ||
|
|
60d35a95df | ||
|
|
920dcd0407 | ||
|
|
d19dd2c9d4 | ||
|
|
be43ebb5c5 | ||
|
|
52eb6d8e27 | ||
|
|
3211d63ec1 | ||
|
|
e1228501d2 | ||
|
|
b8797cf337 | ||
|
|
fa06e90e09 | ||
|
|
d464396d6e | ||
|
|
9961312d34 | ||
|
|
211b2269ff | ||
|
|
b3df5a5f07 | ||
|
|
5204990db9 | ||
|
|
94df221da2 | ||
|
|
e7decba5ee | ||
|
|
a3a09e1952 | ||
|
|
b1e55a41a4 | ||
|
|
b5ad92e236 | ||
|
|
084cfb102c | ||
|
|
2722da6ae4 | ||
|
|
1469064176 | ||
|
|
18ef484f3a | ||
|
|
dba74ab8d4 | ||
|
|
dd80ef484c | ||
|
|
6117e223a7 | ||
|
|
7c76202f1c | ||
|
|
5036cc7b44 | ||
|
|
8a8ea2913c | ||
|
|
b2dd395a86 | ||
|
|
d21fb4154e | ||
|
|
7edc69454d | ||
|
|
147cd1a580 | ||
|
|
0e40da1c70 | ||
|
|
4dcb23cfe5 | ||
|
|
3dcfcff04c | ||
|
|
2c50972e08 | ||
|
|
b9ac40899d | ||
|
|
e25eb2a948 | ||
|
|
440c59ecb9 | ||
|
|
c9198d232f | ||
|
|
e689dadf00 | ||
|
|
d90c05e0a4 | ||
|
|
57ce51d4ef | ||
|
|
7d2830b8b2 | ||
|
|
7688bdce45 | ||
|
|
eecaca7c7b | ||
|
|
729aa31d86 | ||
|
|
94df60292e | ||
|
|
2d9acd9989 | ||
|
|
09f2eb3601 | ||
|
|
066c3de113 | ||
|
|
9d8175354b | ||
|
|
dcd16f29c2 | ||
|
|
5d513da73c | ||
|
|
3e904e8a14 | ||
|
|
f16b99bec0 | ||
|
|
59152214cf | ||
|
|
80f701150c | ||
|
|
df1cdb5d48 | ||
|
|
7976a4811e | ||
|
|
d316ddf02a | ||
|
|
efa0b93fda | ||
|
|
8ab7d3398f | ||
|
|
f0bebd0d96 | ||
|
|
5d249c5401 | ||
|
|
bd9e9d4a06 | ||
|
|
9b3014d5bf | ||
|
|
020830d591 | ||
|
|
ced65ea401 | ||
|
|
73ec884ea4 | ||
|
|
74d5e35eb8 | ||
|
|
25b8165547 | ||
|
|
8612712f44 | ||
|
|
67ac8f3397 | ||
|
|
5fe2a70374 | ||
|
|
5b9590a768 | ||
|
|
d90d67d619 | ||
|
|
dd1cbf0a1d | ||
|
|
8bd9c4449c | ||
|
|
c7f6cff15a | ||
|
|
c71be83f43 | ||
|
|
3511811090 | ||
|
|
c350847b50 | ||
|
|
8623829696 | ||
|
|
235522dfd7 | ||
|
|
edd1fbe49c | ||
|
|
dae18a05c6 | ||
|
|
c5b60b8a12 | ||
|
|
40a87761c9 | ||
|
|
d9b5c77e3a | ||
|
|
5b6e8f1a31 | ||
|
|
d533c60824 | ||
|
|
539273b963 | ||
|
|
71331d37e4 | ||
|
|
4153bf27a9 | ||
|
|
938e029592 | ||
|
|
d3bc7cb82b | ||
|
|
36110c8727 | ||
|
|
4a944fbbdb | ||
|
|
91976e595a | ||
|
|
a492f0a6d1 | ||
|
|
6764cd961f | ||
|
|
a125b367e3 | ||
|
|
07b30d6ebd | ||
|
|
3640e9c056 | ||
|
|
90a1b73d21 | ||
|
|
b3bae50ca1 | ||
|
|
2b9ed927c7 | ||
|
|
706b4cf3e6 | ||
|
|
c1b9aa5fba | ||
|
|
77a44362aa | ||
|
|
a9fb254d76 | ||
|
|
b0a8bfbcf4 | ||
|
|
a7f89e4327 | ||
|
|
af1ec22d41 | ||
|
|
f55334cd5b | ||
|
|
4a2e32790a | ||
|
|
760688d9e6 | ||
|
|
279de0a641 | ||
|
|
b2f31e7782 | ||
|
|
68d8f65cb9 | ||
|
|
e97e4ecdc8 | ||
|
|
e77c998a94 | ||
|
|
861d7b92f1 | ||
|
|
bfc882306b | ||
|
|
14e9cf435e | ||
|
|
83b31175df | ||
|
|
92114f9018 | ||
|
|
58a80c349d | ||
|
|
5f34fca9e7 | ||
|
|
fd6389249c | ||
|
|
374f13adbf | ||
|
|
17e443a7e2 | ||
|
|
4628f1cc8d | ||
|
|
e97411a502 | ||
|
|
304cde64fe | ||
|
|
3dbce922c3 | ||
|
|
4e57ac77f0 | ||
|
|
de960b2b57 | ||
|
|
9b60adeb45 | ||
|
|
0333bb5e99 | ||
|
|
b1edaddb31 | ||
|
|
01c2a2b1cb | ||
|
|
541b29b2e9 | ||
|
|
6f3afd2fa1 | ||
|
|
882d460a67 | ||
|
|
9b11ca18d1 | ||
|
|
39bd7c55f6 | ||
|
|
3654d6bf6d | ||
|
|
1ca368fa45 | ||
|
|
698ea51cb9 | ||
|
|
e6be0179b7 | ||
|
|
97e1bd2bf0 | ||
|
|
5695bca9f3 | ||
|
|
75d9dda4ba | ||
|
|
41a50ffa5a | ||
|
|
3daaa6fbbc | ||
|
|
341030058b | ||
|
|
fd98e8e78f | ||
|
|
e24c6a2cad | ||
|
|
01e549275e | ||
|
|
37780986fc | ||
|
|
9f34e3303e | ||
|
|
d5a8d96bd5 | ||
|
|
e9207f095f | ||
|
|
ce13c0c53c | ||
|
|
7d5bf0e8df | ||
|
|
bdf31864eb | ||
|
|
32388b884f | ||
|
|
7e941f5a81 | ||
|
|
a1920b97a4 | ||
|
|
e0ed4cf16c | ||
|
|
e471bd5ad3 | ||
|
|
9a72f85a0e | ||
|
|
9ae7b23939 | ||
|
|
9dc7456edb | ||
|
|
12d34143c2 | ||
|
|
7862bdb173 | ||
|
|
e143096140 | ||
|
|
5b5d540310 | ||
|
|
d84136b2dd | ||
|
|
fbbbfdf53c | ||
|
|
2dbd510486 | ||
|
|
e748b53b6a | ||
|
|
5aff8273b0 | ||
|
|
1f89dba702 | ||
|
|
90c5cc3c72 | ||
|
|
d7b1e2002d | ||
|
|
ec2e09edcf | ||
|
|
1928db63bb | ||
|
|
ca54088bdc | ||
|
|
1a26d7336c | ||
|
|
bb71c40b56 | ||
|
|
2e47642f74 | ||
|
|
9939eab8ec | ||
|
|
c61d6a93b2 | ||
|
|
bbdd3873e2 | ||
|
|
43bfd6b5ab | ||
|
|
dd99417e7b | ||
|
|
8cf1997460 | ||
|
|
ce38f5880f | ||
|
|
79f143b1f8 | ||
|
|
ad34fc09b6 | ||
|
|
566ef02b94 | ||
|
|
6300c217b3 | ||
|
|
d967400cb4 | ||
|
|
9ee98a2925 | ||
|
|
ee9993d7c2 | ||
|
|
09d2592635 | ||
|
|
0f64768dd8 | ||
|
|
ccdac0305f | ||
|
|
d28bd3ab52 | ||
|
|
4ce6826f82 | ||
|
|
adeb588471 | ||
|
|
57329482f3 | ||
|
|
a894c2d206 | ||
|
|
86fb80d7e4 | ||
|
|
dcc48e37df | ||
|
|
117124d1dd | ||
|
|
0d9af81ae7 | ||
|
|
9eddc03cf1 | ||
|
|
42864997b3 | ||
|
|
58882f508e |
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: 🐛 Bug report
|
||||||
|
about: Create a report to help us improve 🤕
|
||||||
|
labels: bug
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
(e.g. Instaloader command line)
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen (if not obvious).
|
||||||
|
|
||||||
|
**Error messages and tracebacks**
|
||||||
|
If applicable, add error messages and tracebacks to help explain your problem.
|
||||||
|
|
||||||
|
**Instaloader version**
|
||||||
|
`instaloader --version` output.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
|
|
||||||
|
<!-- please also see https://instaloader.github.io/contributing.html for how to report a bug -->
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Contributing to Instaloader
|
||||||
|
url: https://instaloader.github.io/contributing.html
|
||||||
|
about: Guidelines for how to report an issue.
|
||||||
|
- name: Instaloader Troubleshooting
|
||||||
|
url: https://instaloader.github.io/troubleshooting.html
|
||||||
|
about: Many common problems and their workarounds are described here.
|
||||||
|
- name: Instaloader Documentation
|
||||||
|
url: https://instaloader.github.io/
|
||||||
|
about: Many questions might be already answered here.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
name: 🚀 Feature Suggestion
|
||||||
|
about: Suggest an idea for this project 💡
|
||||||
|
labels: "feature suggestion"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Provide us a use case of the feature**
|
||||||
|
How could the user invoke the new function? Which problem would it solve? If new information is obtained, how would it be further processed?
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**If the feature request is accepted, would you be willing to submit a pull request?**
|
||||||
|
Yes / No
|
||||||
|
<!-- Help can be provided if you need assistance submitting a PR -->
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the feature request here.
|
||||||
|
|
||||||
|
<!-- please also see https://instaloader.github.io/contributing.html for how to suggest a feature -->
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: ❓ Question
|
||||||
|
about: If you have a question about how to use Instaloader that's not well covered by our documentation
|
||||||
|
labels: question
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Your question here...
|
||||||
|
|
||||||
|
<!-- Please see our documentation: https://instaloader.github.io/ -->
|
||||||
|
|
||||||
|
<!-- Basic questions regarding Instaloader's usage might be more appropriate to ask on Stack Overflow -->
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please describe:
|
||||||
|
|
||||||
|
- A motivation for this change, e.g.
|
||||||
|
- Fixes # .
|
||||||
|
- More general: What problem does the pull request solve?
|
||||||
|
|
||||||
|
- The changes proposed in this pull request
|
||||||
|
|
||||||
|
- The completeness of this change
|
||||||
|
- Is it just a proof of concept?
|
||||||
|
- Is the documentation updated (if appropriate)?
|
||||||
|
- Do you consider it ready to be merged or is it a draft?
|
||||||
|
- Can we help you at some point?
|
||||||
|
-->
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: PyLint and MyPy
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: PyLint and MyPy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.8]
|
||||||
|
steps:
|
||||||
|
- name: Checkout Instaloader Repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # needed for building docs
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install pipenv==2020.6.2
|
||||||
|
pipenv sync --dev
|
||||||
|
- name: PyLint
|
||||||
|
run: pipenv run pylint instaloader
|
||||||
|
- name: MyPy
|
||||||
|
run: pipenv run mypy -m instaloader
|
||||||
|
- name: Build Documentation
|
||||||
|
run: pipenv run make -C docs html SPHINXOPTS="-W -n"
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: Mark stale issues and pull requests
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "10 14 * * *"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stale:
|
||||||
|
|
||||||
|
name: Mark stale issues and pull requests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/stale@v3
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
only-labels: 'question'
|
||||||
|
stale-issue-message: 'There has been no activity on this question for an extended period of time. This issue will be closed after further 14 days of inactivity.'
|
||||||
|
stale-issue-label: 'stale'
|
||||||
|
exempt-issue-labels: 'leave open'
|
||||||
|
days-before-stale: 15
|
||||||
|
days-before-close: -1
|
||||||
|
remove-stale-when-updated: false
|
||||||
|
- uses: actions/stale@v1
|
||||||
|
with:
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
stale-issue-message: 'There has been no activity on this issue for an extended period of time. This issue will be closed after further 14 days of inactivity.'
|
||||||
|
stale-pr-message: 'There has been no activity on this Pull Request for an extended period of time. This Pull Request will be closed after further 14 days of inactivity.'
|
||||||
|
stale-issue-label: 'stale'
|
||||||
|
stale-pr-label: 'stale'
|
||||||
|
exempt-issue-label: 'leave open'
|
||||||
|
exempt-pr-label: 'leave open'
|
||||||
|
days-before-stale: 90
|
||||||
|
days-before-close: 14
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
name: Windows EXE build
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Windows EXE build
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Instaloader repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v1
|
||||||
|
with:
|
||||||
|
python-version: 3.8
|
||||||
|
architecture: x64
|
||||||
|
- name: Get the tagged version
|
||||||
|
id: get_version
|
||||||
|
env:
|
||||||
|
GITHUB_REF: ${{ github.ref }}
|
||||||
|
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/}
|
||||||
|
shell: bash
|
||||||
|
- name: Run custom python script for EXE creation
|
||||||
|
env:
|
||||||
|
VERSION_TAG: v${{ steps.get_version.outputs.VERSION }}
|
||||||
|
run: python deploy/windows/create_exe.py
|
||||||
|
- name: Create draft release
|
||||||
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
||||||
|
id: create_release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: v${{ steps.get_version.outputs.VERSION }}
|
||||||
|
release_name: Version ${{ steps.get_version.outputs.VERSION }}
|
||||||
|
draft: true
|
||||||
|
prerelease: contains(steps.get_version.outputs.VERSION, 'a') || contains(steps.get_version.outputs.VERSION, 'b') || contains(steps.get_version.outputs.VERSION, 'c')
|
||||||
|
- name: Upload EXE as release asset
|
||||||
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
||||||
|
id: upload-release-asset
|
||||||
|
uses: actions/upload-release-asset@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
|
asset_path: ./instaloader-v${{ steps.get_version.outputs.VERSION }}-windows-standalone.zip
|
||||||
|
asset_name: instaloader-v${{ steps.get_version.outputs.VERSION }}-windows-standalone.zip
|
||||||
|
asset_content_type: application/zip
|
||||||
@@ -1 +1,2 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
|
.mypy_cache/
|
||||||
|
|||||||
@@ -0,0 +1,584 @@
|
|||||||
|
[MASTER]
|
||||||
|
|
||||||
|
# A comma-separated list of package or module names from where C extensions may
|
||||||
|
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||||
|
# run arbitrary code.
|
||||||
|
extension-pkg-whitelist=
|
||||||
|
|
||||||
|
# Add files or directories to the blacklist. They should be base names, not
|
||||||
|
# paths.
|
||||||
|
ignore=CVS
|
||||||
|
|
||||||
|
# Add files or directories matching the regex patterns to the blacklist. The
|
||||||
|
# regex matches against base names, not paths.
|
||||||
|
ignore-patterns=
|
||||||
|
|
||||||
|
# Python code to execute, usually for sys.path manipulation such as
|
||||||
|
# pygtk.require().
|
||||||
|
#init-hook=
|
||||||
|
|
||||||
|
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||||
|
# number of processors available to use.
|
||||||
|
jobs=0
|
||||||
|
|
||||||
|
# Control the amount of potential inferred values when inferring a single
|
||||||
|
# object. This can help the performance when dealing with large functions or
|
||||||
|
# complex, nested conditions.
|
||||||
|
limit-inference-results=100
|
||||||
|
|
||||||
|
# List of plugins (as comma separated values of python modules names) to load,
|
||||||
|
# usually to register additional checkers.
|
||||||
|
load-plugins=
|
||||||
|
|
||||||
|
# Pickle collected data for later comparisons.
|
||||||
|
persistent=yes
|
||||||
|
|
||||||
|
# Specify a configuration file.
|
||||||
|
#rcfile=
|
||||||
|
|
||||||
|
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||||
|
# user-friendly hints instead of false-positive error messages.
|
||||||
|
suggestion-mode=yes
|
||||||
|
|
||||||
|
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||||
|
# active Python interpreter and may run arbitrary code.
|
||||||
|
unsafe-load-any-extension=no
|
||||||
|
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
|
||||||
|
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||||
|
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
|
||||||
|
confidence=
|
||||||
|
|
||||||
|
# Disable the message, report, category or checker with the given id(s). You
|
||||||
|
# can either give multiple identifiers separated by comma (,) or put this
|
||||||
|
# option multiple times (only on the command line, not in the configuration
|
||||||
|
# file where it should appear only once). You can also use "--disable=all" to
|
||||||
|
# disable everything first and then reenable specific checks. For example, if
|
||||||
|
# you want to run only the similarities checker, you can use "--disable=all
|
||||||
|
# --enable=similarities". If you want to run only the classes checker, but have
|
||||||
|
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||||
|
# --disable=W".
|
||||||
|
disable=invalid-name,
|
||||||
|
missing-docstring,
|
||||||
|
too-many-lines,
|
||||||
|
print-statement,
|
||||||
|
parameter-unpacking,
|
||||||
|
unpacking-in-except,
|
||||||
|
old-raise-syntax,
|
||||||
|
backtick,
|
||||||
|
long-suffix,
|
||||||
|
old-ne-operator,
|
||||||
|
old-octal-literal,
|
||||||
|
import-star-module-level,
|
||||||
|
non-ascii-bytes-literal,
|
||||||
|
raw-checker-failed,
|
||||||
|
bad-inline-option,
|
||||||
|
locally-disabled,
|
||||||
|
file-ignored,
|
||||||
|
suppressed-message,
|
||||||
|
useless-suppression,
|
||||||
|
deprecated-pragma,
|
||||||
|
use-symbolic-message-instead,
|
||||||
|
too-many-instance-attributes,
|
||||||
|
too-many-public-methods,
|
||||||
|
too-many-branches,
|
||||||
|
too-many-arguments,
|
||||||
|
too-many-locals,
|
||||||
|
too-many-statements,
|
||||||
|
no-else-return,
|
||||||
|
no-else-raise,
|
||||||
|
unnecessary-pass,
|
||||||
|
wildcard-import,
|
||||||
|
unused-wildcard-import,
|
||||||
|
apply-builtin,
|
||||||
|
basestring-builtin,
|
||||||
|
buffer-builtin,
|
||||||
|
cmp-builtin,
|
||||||
|
coerce-builtin,
|
||||||
|
execfile-builtin,
|
||||||
|
file-builtin,
|
||||||
|
long-builtin,
|
||||||
|
raw_input-builtin,
|
||||||
|
reduce-builtin,
|
||||||
|
standarderror-builtin,
|
||||||
|
unicode-builtin,
|
||||||
|
xrange-builtin,
|
||||||
|
coerce-method,
|
||||||
|
delslice-method,
|
||||||
|
getslice-method,
|
||||||
|
setslice-method,
|
||||||
|
no-absolute-import,
|
||||||
|
old-division,
|
||||||
|
dict-iter-method,
|
||||||
|
dict-view-method,
|
||||||
|
next-method-called,
|
||||||
|
metaclass-assignment,
|
||||||
|
indexing-exception,
|
||||||
|
raising-string,
|
||||||
|
reload-builtin,
|
||||||
|
oct-method,
|
||||||
|
hex-method,
|
||||||
|
nonzero-method,
|
||||||
|
cmp-method,
|
||||||
|
input-builtin,
|
||||||
|
round-builtin,
|
||||||
|
intern-builtin,
|
||||||
|
unichr-builtin,
|
||||||
|
map-builtin-not-iterating,
|
||||||
|
zip-builtin-not-iterating,
|
||||||
|
range-builtin-not-iterating,
|
||||||
|
filter-builtin-not-iterating,
|
||||||
|
using-cmp-argument,
|
||||||
|
eq-without-hash,
|
||||||
|
div-method,
|
||||||
|
idiv-method,
|
||||||
|
rdiv-method,
|
||||||
|
exception-message-attribute,
|
||||||
|
invalid-str-codec,
|
||||||
|
sys-max-int,
|
||||||
|
bad-python3-import,
|
||||||
|
deprecated-string-function,
|
||||||
|
deprecated-str-translate-call,
|
||||||
|
deprecated-itertools-function,
|
||||||
|
deprecated-types-field,
|
||||||
|
next-method-defined,
|
||||||
|
dict-items-not-iterating,
|
||||||
|
dict-keys-not-iterating,
|
||||||
|
dict-values-not-iterating,
|
||||||
|
deprecated-operator-function,
|
||||||
|
deprecated-urllib-function,
|
||||||
|
xreadlines-attribute,
|
||||||
|
deprecated-sys-function,
|
||||||
|
exception-escape,
|
||||||
|
comprehension-escape
|
||||||
|
|
||||||
|
# Enable the message, report, category or checker with the given id(s). You can
|
||||||
|
# either give multiple identifier separated by comma (,) or put this option
|
||||||
|
# multiple time (only on the command line, not in the configuration file where
|
||||||
|
# it should appear only once). See also the "--disable" option for examples.
|
||||||
|
enable=c-extension-no-member
|
||||||
|
|
||||||
|
|
||||||
|
[REPORTS]
|
||||||
|
|
||||||
|
# Python expression which should return a note less than 10 (10 is the highest
|
||||||
|
# note). You have access to the variables errors warning, statement which
|
||||||
|
# respectively contain the number of errors / warnings messages and the total
|
||||||
|
# number of statements analyzed. This is used by the global evaluation report
|
||||||
|
# (RP0004).
|
||||||
|
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||||
|
|
||||||
|
# Template used to display messages. This is a python new-style format string
|
||||||
|
# used to format the message information. See doc for all details.
|
||||||
|
#msg-template=
|
||||||
|
|
||||||
|
# Set the output format. Available formats are text, parseable, colorized, json
|
||||||
|
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||||
|
# mypackage.mymodule.MyReporterClass.
|
||||||
|
output-format=text
|
||||||
|
|
||||||
|
# Tells whether to display a full report or only the messages.
|
||||||
|
reports=no
|
||||||
|
|
||||||
|
# Activate the evaluation score.
|
||||||
|
score=yes
|
||||||
|
|
||||||
|
|
||||||
|
[REFACTORING]
|
||||||
|
|
||||||
|
# Maximum number of nested blocks for function / method body
|
||||||
|
max-nested-blocks=5
|
||||||
|
|
||||||
|
# Complete name of functions that never returns. When checking for
|
||||||
|
# inconsistent-return-statements if a never returning function is called then
|
||||||
|
# it will be considered as an explicit return statement and no message will be
|
||||||
|
# printed.
|
||||||
|
never-returning-functions=sys.exit
|
||||||
|
|
||||||
|
|
||||||
|
[VARIABLES]
|
||||||
|
|
||||||
|
# List of additional names supposed to be defined in builtins. Remember that
|
||||||
|
# you should avoid defining new builtins when possible.
|
||||||
|
additional-builtins=
|
||||||
|
|
||||||
|
# Tells whether unused global variables should be treated as a violation.
|
||||||
|
allow-global-unused-variables=yes
|
||||||
|
|
||||||
|
# List of strings which can identify a callback function by name. A callback
|
||||||
|
# name must start or end with one of those strings.
|
||||||
|
callbacks=cb_,
|
||||||
|
_cb
|
||||||
|
|
||||||
|
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||||
|
# not be used).
|
||||||
|
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||||
|
|
||||||
|
# Argument names that match this expression will be ignored. Default to name
|
||||||
|
# with leading underscore.
|
||||||
|
ignored-argument-names=_.*|^ignored_|^unused_
|
||||||
|
|
||||||
|
# Tells whether we should check for unused import in __init__ files.
|
||||||
|
init-import=no
|
||||||
|
|
||||||
|
# List of qualified module names which can have objects that can redefine
|
||||||
|
# builtins.
|
||||||
|
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||||
|
|
||||||
|
|
||||||
|
[TYPECHECK]
|
||||||
|
|
||||||
|
# List of decorators that produce context managers, such as
|
||||||
|
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||||
|
# produce valid context managers.
|
||||||
|
contextmanager-decorators=contextlib.contextmanager
|
||||||
|
|
||||||
|
# List of members which are set dynamically and missed by pylint inference
|
||||||
|
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||||
|
# expressions are accepted.
|
||||||
|
generated-members=
|
||||||
|
|
||||||
|
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||||
|
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||||
|
ignore-mixin-members=yes
|
||||||
|
|
||||||
|
# Tells whether to warn about missing members when the owner of the attribute
|
||||||
|
# is inferred to be None.
|
||||||
|
ignore-none=yes
|
||||||
|
|
||||||
|
# This flag controls whether pylint should warn about no-member and similar
|
||||||
|
# checks whenever an opaque object is returned when inferring. The inference
|
||||||
|
# can return multiple potential results while evaluating a Python object, but
|
||||||
|
# some branches might not be evaluated, which results in partial inference. In
|
||||||
|
# that case, it might be useful to still emit no-member and other checks for
|
||||||
|
# the rest of the inferred objects.
|
||||||
|
ignore-on-opaque-inference=yes
|
||||||
|
|
||||||
|
# List of class names for which member attributes should not be checked (useful
|
||||||
|
# for classes with dynamically set attributes). This supports the use of
|
||||||
|
# qualified names.
|
||||||
|
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||||
|
|
||||||
|
# List of module names for which member attributes should not be checked
|
||||||
|
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||||
|
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||||
|
# supports qualified module names, as well as Unix pattern matching.
|
||||||
|
ignored-modules=
|
||||||
|
|
||||||
|
# Show a hint with possible names when a member name was not found. The aspect
|
||||||
|
# of finding the hint is based on edit distance.
|
||||||
|
missing-member-hint=yes
|
||||||
|
|
||||||
|
# The minimum edit distance a name should have in order to be considered a
|
||||||
|
# similar match for a missing member name.
|
||||||
|
missing-member-hint-distance=1
|
||||||
|
|
||||||
|
# The total number of similar names that should be taken in consideration when
|
||||||
|
# showing a hint for a missing member.
|
||||||
|
missing-member-max-choices=1
|
||||||
|
|
||||||
|
|
||||||
|
[STRING]
|
||||||
|
|
||||||
|
# This flag controls whether the implicit-str-concat-in-sequence should
|
||||||
|
# generate a warning on implicit string concatenation in sequences defined over
|
||||||
|
# several lines.
|
||||||
|
check-str-concat-over-line-jumps=no
|
||||||
|
|
||||||
|
|
||||||
|
[SPELLING]
|
||||||
|
|
||||||
|
# Limits count of emitted suggestions for spelling mistakes.
|
||||||
|
max-spelling-suggestions=4
|
||||||
|
|
||||||
|
# Spelling dictionary name. Available dictionaries: none. To make it working
|
||||||
|
# install python-enchant package..
|
||||||
|
spelling-dict=
|
||||||
|
|
||||||
|
# List of comma separated words that should not be checked.
|
||||||
|
spelling-ignore-words=
|
||||||
|
|
||||||
|
# A path to a file that contains private dictionary; one word per line.
|
||||||
|
spelling-private-dict-file=
|
||||||
|
|
||||||
|
# Tells whether to store unknown words to indicated private dictionary in
|
||||||
|
# --spelling-private-dict-file option instead of raising a message.
|
||||||
|
spelling-store-unknown-words=no
|
||||||
|
|
||||||
|
|
||||||
|
[SIMILARITIES]
|
||||||
|
|
||||||
|
# Ignore comments when computing similarities.
|
||||||
|
ignore-comments=yes
|
||||||
|
|
||||||
|
# Ignore docstrings when computing similarities.
|
||||||
|
ignore-docstrings=yes
|
||||||
|
|
||||||
|
# Ignore imports when computing similarities.
|
||||||
|
ignore-imports=no
|
||||||
|
|
||||||
|
# Minimum lines number of a similarity.
|
||||||
|
min-similarity-lines=4
|
||||||
|
|
||||||
|
|
||||||
|
[MISCELLANEOUS]
|
||||||
|
|
||||||
|
# List of note tags to take in consideration, separated by a comma.
|
||||||
|
notes=FIXME,
|
||||||
|
XXX,
|
||||||
|
TODO
|
||||||
|
|
||||||
|
|
||||||
|
[LOGGING]
|
||||||
|
|
||||||
|
# Format style used to check logging format string. `old` means using %
|
||||||
|
# formatting, while `new` is for `{}` formatting.
|
||||||
|
logging-format-style=old
|
||||||
|
|
||||||
|
# Logging modules to check that the string format arguments are in logging
|
||||||
|
# function parameter format.
|
||||||
|
logging-modules=logging
|
||||||
|
|
||||||
|
|
||||||
|
[FORMAT]
|
||||||
|
|
||||||
|
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||||
|
expected-line-ending-format=
|
||||||
|
|
||||||
|
# Regexp for a line that is allowed to be longer than the limit.
|
||||||
|
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||||
|
|
||||||
|
# Number of spaces of indent required inside a hanging or continued line.
|
||||||
|
indent-after-paren=4
|
||||||
|
|
||||||
|
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||||
|
# tab).
|
||||||
|
indent-string=' '
|
||||||
|
|
||||||
|
# Maximum number of characters on a single line.
|
||||||
|
max-line-length=120
|
||||||
|
|
||||||
|
# Maximum number of lines in a module.
|
||||||
|
max-module-lines=1000
|
||||||
|
|
||||||
|
# List of optional constructs for which whitespace checking is disabled. `dict-
|
||||||
|
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
|
||||||
|
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
|
||||||
|
# `empty-line` allows space-only lines.
|
||||||
|
no-space-check=trailing-comma,
|
||||||
|
dict-separator
|
||||||
|
|
||||||
|
# Allow the body of a class to be on the same line as the declaration if body
|
||||||
|
# contains single statement.
|
||||||
|
single-line-class-stmt=no
|
||||||
|
|
||||||
|
# Allow the body of an if to be on the same line as the test if there is no
|
||||||
|
# else.
|
||||||
|
single-line-if-stmt=no
|
||||||
|
|
||||||
|
|
||||||
|
[BASIC]
|
||||||
|
|
||||||
|
# Naming style matching correct argument names.
|
||||||
|
argument-naming-style=snake_case
|
||||||
|
|
||||||
|
# Regular expression matching correct argument names. Overrides argument-
|
||||||
|
# naming-style.
|
||||||
|
#argument-rgx=
|
||||||
|
|
||||||
|
# Naming style matching correct attribute names.
|
||||||
|
attr-naming-style=snake_case
|
||||||
|
|
||||||
|
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||||
|
# style.
|
||||||
|
#attr-rgx=
|
||||||
|
|
||||||
|
# Bad variable names which should always be refused, separated by a comma.
|
||||||
|
bad-names=foo,
|
||||||
|
bar,
|
||||||
|
baz,
|
||||||
|
toto,
|
||||||
|
tutu,
|
||||||
|
tata
|
||||||
|
|
||||||
|
# Naming style matching correct class attribute names.
|
||||||
|
class-attribute-naming-style=any
|
||||||
|
|
||||||
|
# Regular expression matching correct class attribute names. Overrides class-
|
||||||
|
# attribute-naming-style.
|
||||||
|
#class-attribute-rgx=
|
||||||
|
|
||||||
|
# Naming style matching correct class names.
|
||||||
|
class-naming-style=PascalCase
|
||||||
|
|
||||||
|
# Regular expression matching correct class names. Overrides class-naming-
|
||||||
|
# style.
|
||||||
|
#class-rgx=
|
||||||
|
|
||||||
|
# Naming style matching correct constant names.
|
||||||
|
const-naming-style=UPPER_CASE
|
||||||
|
|
||||||
|
# Regular expression matching correct constant names. Overrides const-naming-
|
||||||
|
# style.
|
||||||
|
#const-rgx=
|
||||||
|
|
||||||
|
# Minimum line length for functions/classes that require docstrings, shorter
|
||||||
|
# ones are exempt.
|
||||||
|
docstring-min-length=-1
|
||||||
|
|
||||||
|
# Naming style matching correct function names.
|
||||||
|
function-naming-style=snake_case
|
||||||
|
|
||||||
|
# Regular expression matching correct function names. Overrides function-
|
||||||
|
# naming-style.
|
||||||
|
#function-rgx=
|
||||||
|
|
||||||
|
# Good variable names which should always be accepted, separated by a comma.
|
||||||
|
good-names=i,
|
||||||
|
j,
|
||||||
|
k,
|
||||||
|
ex,
|
||||||
|
Run,
|
||||||
|
_
|
||||||
|
|
||||||
|
# Include a hint for the correct naming format with invalid-name.
|
||||||
|
include-naming-hint=no
|
||||||
|
|
||||||
|
# Naming style matching correct inline iteration names.
|
||||||
|
inlinevar-naming-style=any
|
||||||
|
|
||||||
|
# Regular expression matching correct inline iteration names. Overrides
|
||||||
|
# inlinevar-naming-style.
|
||||||
|
#inlinevar-rgx=
|
||||||
|
|
||||||
|
# Naming style matching correct method names.
|
||||||
|
method-naming-style=snake_case
|
||||||
|
|
||||||
|
# Regular expression matching correct method names. Overrides method-naming-
|
||||||
|
# style.
|
||||||
|
#method-rgx=
|
||||||
|
|
||||||
|
# Naming style matching correct module names.
|
||||||
|
module-naming-style=snake_case
|
||||||
|
|
||||||
|
# Regular expression matching correct module names. Overrides module-naming-
|
||||||
|
# style.
|
||||||
|
#module-rgx=
|
||||||
|
|
||||||
|
# Colon-delimited sets of names that determine each other's naming style when
|
||||||
|
# the name regexes allow several styles.
|
||||||
|
name-group=
|
||||||
|
|
||||||
|
# Regular expression which should only match function or class names that do
|
||||||
|
# not require a docstring.
|
||||||
|
no-docstring-rgx=^_
|
||||||
|
|
||||||
|
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||||
|
# to this list to register other decorators that produce valid properties.
|
||||||
|
# These decorators are taken in consideration only for invalid-name.
|
||||||
|
property-classes=abc.abstractproperty
|
||||||
|
|
||||||
|
# Naming style matching correct variable names.
|
||||||
|
variable-naming-style=snake_case
|
||||||
|
|
||||||
|
# Regular expression matching correct variable names. Overrides variable-
|
||||||
|
# naming-style.
|
||||||
|
#variable-rgx=
|
||||||
|
|
||||||
|
|
||||||
|
[IMPORTS]
|
||||||
|
|
||||||
|
# Allow wildcard imports from modules that define __all__.
|
||||||
|
allow-wildcard-with-all=no
|
||||||
|
|
||||||
|
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||||
|
# 3 compatible code, which means that the block might have code that exists
|
||||||
|
# only in one or another interpreter, leading to false positives when analysed.
|
||||||
|
analyse-fallback-blocks=no
|
||||||
|
|
||||||
|
# Deprecated modules which should not be used, separated by a comma.
|
||||||
|
deprecated-modules=optparse,tkinter.tix
|
||||||
|
|
||||||
|
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||||
|
# not be disabled).
|
||||||
|
ext-import-graph=
|
||||||
|
|
||||||
|
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||||
|
# given file (report RP0402 must not be disabled).
|
||||||
|
import-graph=
|
||||||
|
|
||||||
|
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||||
|
# not be disabled).
|
||||||
|
int-import-graph=
|
||||||
|
|
||||||
|
# Force import order to recognize a module as part of the standard
|
||||||
|
# compatibility libraries.
|
||||||
|
known-standard-library=
|
||||||
|
|
||||||
|
# Force import order to recognize a module as part of a third party library.
|
||||||
|
known-third-party=enchant
|
||||||
|
|
||||||
|
|
||||||
|
[DESIGN]
|
||||||
|
|
||||||
|
# Maximum number of arguments for function / method.
|
||||||
|
max-args=5
|
||||||
|
|
||||||
|
# Maximum number of attributes for a class (see R0902).
|
||||||
|
max-attributes=7
|
||||||
|
|
||||||
|
# Maximum number of boolean expressions in an if statement.
|
||||||
|
max-bool-expr=5
|
||||||
|
|
||||||
|
# Maximum number of branch for function / method body.
|
||||||
|
max-branches=12
|
||||||
|
|
||||||
|
# Maximum number of locals for function / method body.
|
||||||
|
max-locals=15
|
||||||
|
|
||||||
|
# Maximum number of parents for a class (see R0901).
|
||||||
|
max-parents=7
|
||||||
|
|
||||||
|
# Maximum number of public methods for a class (see R0904).
|
||||||
|
max-public-methods=20
|
||||||
|
|
||||||
|
# Maximum number of return / yield for function / method body.
|
||||||
|
max-returns=6
|
||||||
|
|
||||||
|
# Maximum number of statements in function / method body.
|
||||||
|
max-statements=50
|
||||||
|
|
||||||
|
# Minimum number of public methods for a class (see R0903).
|
||||||
|
min-public-methods=2
|
||||||
|
|
||||||
|
|
||||||
|
[CLASSES]
|
||||||
|
|
||||||
|
# List of method names used to declare (i.e. assign) instance attributes.
|
||||||
|
defining-attr-methods=__init__,
|
||||||
|
__new__,
|
||||||
|
setUp
|
||||||
|
|
||||||
|
# List of member names, which should be excluded from the protected access
|
||||||
|
# warning.
|
||||||
|
exclude-protected=_asdict,
|
||||||
|
_fields,
|
||||||
|
_replace,
|
||||||
|
_source,
|
||||||
|
_make
|
||||||
|
|
||||||
|
# List of valid names for the first argument in a class method.
|
||||||
|
valid-classmethod-first-arg=cls
|
||||||
|
|
||||||
|
# List of valid names for the first argument in a metaclass class method.
|
||||||
|
valid-metaclass-classmethod-first-arg=cls
|
||||||
|
|
||||||
|
|
||||||
|
[EXCEPTIONS]
|
||||||
|
|
||||||
|
# Exceptions that will emit a warning when being caught. Defaults to
|
||||||
|
# "BaseException, Exception".
|
||||||
|
overgeneral-exceptions=BaseException,
|
||||||
|
Exception
|
||||||
+29
-13
@@ -1,20 +1,36 @@
|
|||||||
|
dist: xenial
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- "3.5"
|
- "3.5"
|
||||||
- "3.5-dev"
|
|
||||||
- "3.6"
|
- "3.6"
|
||||||
- "3.6-dev"
|
- "3.7"
|
||||||
- "3.7-dev"
|
|
||||||
- "nightly"
|
|
||||||
install:
|
install:
|
||||||
- pip install pylint requests
|
- pipenv sync --dev
|
||||||
script:
|
script:
|
||||||
- python3 -m pylint -r n -d bad-whitespace,missing-docstring,too-many-arguments,locally-disabled,line-too-long,too-many-public-methods,too-many-lines,too-many-instance-attributes,too-many-locals,too-many-branches,too-many-statements instaloader
|
- pylint instaloader
|
||||||
|
- mypy -m instaloader
|
||||||
|
- make -C docs html SPHINXOPTS="-W -n"
|
||||||
|
addons:
|
||||||
|
ssh_known_hosts: aur.archlinux.org
|
||||||
deploy:
|
deploy:
|
||||||
provider: pypi
|
- provider: pypi
|
||||||
user: aandergr
|
user: aandergr
|
||||||
on:
|
on:
|
||||||
tags: true
|
tags: true
|
||||||
python: 3.6
|
python: 3.6
|
||||||
password:
|
password:
|
||||||
secure: KBsATtXNnEzqr0X2ALdyx2zaYj2EUfK8YjP4jEOilBELpJ4evHYXVgRkn9CKXGsD7LKbWmR3gz2rr8NZ6U42pHw0fpQHizz91MpJSKbNY7RZZh4UVWMDh81izeglchWAF+m3Wdpxpo/GBDbrZIbRQFhPgB+kjSzJo3uiCS+yVXtGG+6csJHzd0WkJjsVHz+XSdxyOiLy/qFF3nmY6ZrVrgw6WbX2YLbx9je6DxK3YRrnBqgMgH7Ui2xWWbgJCk3KKpQTMiMKnTa9mdeMJAd26YMCz1gG/X7ck6VNly/6bs5+Zq7/bU0dSolhPBhNthyccYdS0IeGPdPF3YqatblJ4fLGTfHn8oJs3/T11yIW9pvOT3UftEynPOUX+en2Qnb/3WfgwUfVts8i8DAYgEFLGEzbF5XMGy4gwAD/bZ0JW0pmgAG2PZsdOokAt+ydlNj/uxq1GFW2doZbul6k4t03p1mik6itYscvuhKoEm8Yw6wr1P9aDvMR6FFpSu4gDt2XrKmRmWkSXxpNeCfZujTjyAnISxLVQGSY7BG5K0Z2HbDtqqNz9nj4WgNGzZmngiRofCevWR+vPIDUDW7NCX03csfcFS9RRllBO/BrL7NVUV+Qp9dfTQYp+8SqYLWgHtDoXdgyMREjJaAmbx2u/TYTY8bADZ5F3vGUpv2j5hP3NTU=
|
secure: KBsATtXNnEzqr0X2ALdyx2zaYj2EUfK8YjP4jEOilBELpJ4evHYXVgRkn9CKXGsD7LKbWmR3gz2rr8NZ6U42pHw0fpQHizz91MpJSKbNY7RZZh4UVWMDh81izeglchWAF+m3Wdpxpo/GBDbrZIbRQFhPgB+kjSzJo3uiCS+yVXtGG+6csJHzd0WkJjsVHz+XSdxyOiLy/qFF3nmY6ZrVrgw6WbX2YLbx9je6DxK3YRrnBqgMgH7Ui2xWWbgJCk3KKpQTMiMKnTa9mdeMJAd26YMCz1gG/X7ck6VNly/6bs5+Zq7/bU0dSolhPBhNthyccYdS0IeGPdPF3YqatblJ4fLGTfHn8oJs3/T11yIW9pvOT3UftEynPOUX+en2Qnb/3WfgwUfVts8i8DAYgEFLGEzbF5XMGy4gwAD/bZ0JW0pmgAG2PZsdOokAt+ydlNj/uxq1GFW2doZbul6k4t03p1mik6itYscvuhKoEm8Yw6wr1P9aDvMR6FFpSu4gDt2XrKmRmWkSXxpNeCfZujTjyAnISxLVQGSY7BG5K0Z2HbDtqqNz9nj4WgNGzZmngiRofCevWR+vPIDUDW7NCX03csfcFS9RRllBO/BrL7NVUV+Qp9dfTQYp+8SqYLWgHtDoXdgyMREjJaAmbx2u/TYTY8bADZ5F3vGUpv2j5hP3NTU=
|
||||||
|
- provider: pages
|
||||||
|
skip-cleanup: true
|
||||||
|
github-token: $GITHUB_TOKEN
|
||||||
|
local-dir: docs/_build/html
|
||||||
|
repo: instaloader/instaloader.github.io
|
||||||
|
target-branch: master
|
||||||
|
on:
|
||||||
|
branch: master
|
||||||
|
python: 3.6
|
||||||
|
- provider: script
|
||||||
|
script: deploy/arch/deploy.sh $TRAVIS_TAG
|
||||||
|
on:
|
||||||
|
tags: true
|
||||||
|
python: 3.6
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
Authors
|
||||||
|
=======
|
||||||
|
|
||||||
|
Instaloader is written by
|
||||||
|
|
||||||
|
- Alexander Graf (@aandergr)
|
||||||
|
- André Koch-Kramer (@Thammus)
|
||||||
|
- Lars Lindqvist (@e5150)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2016 Alexander Graf and André Koch-Kramer.
|
Copyright (c) 2016-2019 Alexander Graf and André Koch-Kramer.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[[source]]
|
||||||
|
name = "pypi"
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
pylint = "*"
|
||||||
|
mypy = "*"
|
||||||
|
sphinx = "~=2.3"
|
||||||
|
pyinstaller = "*"
|
||||||
|
pefile = "*"
|
||||||
|
pywin32-ctypes = "*"
|
||||||
|
psutil = "*"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
requests = "*"
|
||||||
Generated
+439
@@ -0,0 +1,439 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "58caf1be034bed9aa7ebb8c78e75ffa7d7abe820d014b6398c444a5db566b306"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
|
||||||
|
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
|
||||||
|
],
|
||||||
|
"version": "==2020.6.20"
|
||||||
|
},
|
||||||
|
"chardet": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||||
|
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||||
|
],
|
||||||
|
"version": "==3.0.4"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||||
|
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||||
|
],
|
||||||
|
"version": "==2.10"
|
||||||
|
},
|
||||||
|
"requests": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
|
||||||
|
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.24.0"
|
||||||
|
},
|
||||||
|
"urllib3": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
|
||||||
|
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
|
||||||
|
],
|
||||||
|
"version": "==1.25.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {
|
||||||
|
"alabaster": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
|
||||||
|
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
|
||||||
|
],
|
||||||
|
"version": "==0.7.12"
|
||||||
|
},
|
||||||
|
"altgraph": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa",
|
||||||
|
"sha256:c623e5f3408ca61d4016f23a681b9adb100802ca3e3da5e718915a9e4052cebe"
|
||||||
|
],
|
||||||
|
"version": "==0.17"
|
||||||
|
},
|
||||||
|
"astroid": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2f4078c2a41bf377eea06d71c9d2ba4eb8f6b1af2135bec27bbbb7d8f12bb703",
|
||||||
|
"sha256:bc58d83eb610252fd8de6363e39d4f1d0619c894b0ed24603b881c02e64c7386"
|
||||||
|
],
|
||||||
|
"version": "==2.4.2"
|
||||||
|
},
|
||||||
|
"babel": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38",
|
||||||
|
"sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"
|
||||||
|
],
|
||||||
|
"version": "==2.8.0"
|
||||||
|
},
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
|
||||||
|
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
|
||||||
|
],
|
||||||
|
"version": "==2020.6.20"
|
||||||
|
},
|
||||||
|
"chardet": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||||
|
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||||
|
],
|
||||||
|
"version": "==3.0.4"
|
||||||
|
},
|
||||||
|
"docutils": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af",
|
||||||
|
"sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"
|
||||||
|
],
|
||||||
|
"version": "==0.16"
|
||||||
|
},
|
||||||
|
"future": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"
|
||||||
|
],
|
||||||
|
"version": "==0.18.2"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||||
|
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||||
|
],
|
||||||
|
"version": "==2.10"
|
||||||
|
},
|
||||||
|
"imagesize": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6965f19a6a2039c7d48bca7dba2473069ff854c36ae6f19d2cde309d998228a1",
|
||||||
|
"sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"
|
||||||
|
],
|
||||||
|
"version": "==1.2.0"
|
||||||
|
},
|
||||||
|
"isort": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1",
|
||||||
|
"sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd"
|
||||||
|
],
|
||||||
|
"version": "==4.3.21"
|
||||||
|
},
|
||||||
|
"jinja2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
|
||||||
|
"sha256:f0a4641d3cf955324a89c04f3d94663aa4d638abe8f733ecd3582848e1c37035"
|
||||||
|
],
|
||||||
|
"version": "==2.11.2"
|
||||||
|
},
|
||||||
|
"lazy-object-proxy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0c4b206227a8097f05c4dbdd323c50edf81f15db3b8dc064d08c62d37e1a504d",
|
||||||
|
"sha256:194d092e6f246b906e8f70884e620e459fc54db3259e60cf69a4d66c3fda3449",
|
||||||
|
"sha256:1be7e4c9f96948003609aa6c974ae59830a6baecc5376c25c92d7d697e684c08",
|
||||||
|
"sha256:4677f594e474c91da97f489fea5b7daa17b5517190899cf213697e48d3902f5a",
|
||||||
|
"sha256:48dab84ebd4831077b150572aec802f303117c8cc5c871e182447281ebf3ac50",
|
||||||
|
"sha256:5541cada25cd173702dbd99f8e22434105456314462326f06dba3e180f203dfd",
|
||||||
|
"sha256:59f79fef100b09564bc2df42ea2d8d21a64fdcda64979c0fa3db7bdaabaf6239",
|
||||||
|
"sha256:8d859b89baf8ef7f8bc6b00aa20316483d67f0b1cbf422f5b4dc56701c8f2ffb",
|
||||||
|
"sha256:9254f4358b9b541e3441b007a0ea0764b9d056afdeafc1a5569eee1cc6c1b9ea",
|
||||||
|
"sha256:9651375199045a358eb6741df3e02a651e0330be090b3bc79f6d0de31a80ec3e",
|
||||||
|
"sha256:97bb5884f6f1cdce0099f86b907aa41c970c3c672ac8b9c8352789e103cf3156",
|
||||||
|
"sha256:9b15f3f4c0f35727d3a0fba4b770b3c4ebbb1fa907dbcc046a1d2799f3edd142",
|
||||||
|
"sha256:a2238e9d1bb71a56cd710611a1614d1194dc10a175c1e08d75e1a7bcc250d442",
|
||||||
|
"sha256:a6ae12d08c0bf9909ce12385803a543bfe99b95fe01e752536a60af2b7797c62",
|
||||||
|
"sha256:ca0a928a3ddbc5725be2dd1cf895ec0a254798915fb3a36af0964a0a4149e3db",
|
||||||
|
"sha256:cb2c7c57005a6804ab66f106ceb8482da55f5314b7fcb06551db1edae4ad1531",
|
||||||
|
"sha256:d74bb8693bf9cf75ac3b47a54d716bbb1a92648d5f781fc799347cfc95952383",
|
||||||
|
"sha256:d945239a5639b3ff35b70a88c5f2f491913eb94871780ebfabb2568bd58afc5a",
|
||||||
|
"sha256:eba7011090323c1dadf18b3b689845fd96a61ba0a1dfbd7f24b921398affc357",
|
||||||
|
"sha256:efa1909120ce98bbb3777e8b6f92237f5d5c8ea6758efea36a473e1d38f7d3e4",
|
||||||
|
"sha256:f3900e8a5de27447acbf900b4750b0ddfd7ec1ea7fbaf11dfa911141bc522af0"
|
||||||
|
],
|
||||||
|
"version": "==1.4.3"
|
||||||
|
},
|
||||||
|
"markupsafe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
|
||||||
|
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
||||||
|
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
||||||
|
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
||||||
|
"sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42",
|
||||||
|
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
||||||
|
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
||||||
|
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
||||||
|
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
||||||
|
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
||||||
|
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
||||||
|
"sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b",
|
||||||
|
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
||||||
|
"sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15",
|
||||||
|
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
||||||
|
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
||||||
|
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
||||||
|
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
|
||||||
|
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
|
||||||
|
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
|
||||||
|
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
|
||||||
|
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
|
||||||
|
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
|
||||||
|
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
|
||||||
|
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
|
||||||
|
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
|
||||||
|
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
|
||||||
|
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
||||||
|
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
||||||
|
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
||||||
|
"sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2",
|
||||||
|
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7",
|
||||||
|
"sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"
|
||||||
|
],
|
||||||
|
"version": "==1.1.1"
|
||||||
|
},
|
||||||
|
"mccabe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||||
|
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||||
|
],
|
||||||
|
"version": "==0.6.1"
|
||||||
|
},
|
||||||
|
"mypy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2c6cde8aa3426c1682d35190b59b71f661237d74b053822ea3d748e2c9578a7c",
|
||||||
|
"sha256:3fdda71c067d3ddfb21da4b80e2686b71e9e5c72cca65fa216d207a358827f86",
|
||||||
|
"sha256:5dd13ff1f2a97f94540fd37a49e5d255950ebcdf446fb597463a40d0df3fac8b",
|
||||||
|
"sha256:6731603dfe0ce4352c555c6284c6db0dc935b685e9ce2e4cf220abe1e14386fd",
|
||||||
|
"sha256:6bb93479caa6619d21d6e7160c552c1193f6952f0668cdda2f851156e85186fc",
|
||||||
|
"sha256:81c7908b94239c4010e16642c9102bfc958ab14e36048fa77d0be3289dda76ea",
|
||||||
|
"sha256:9c7a9a7ceb2871ba4bac1cf7217a7dd9ccd44c27c2950edbc6dc08530f32ad4e",
|
||||||
|
"sha256:a4a2cbcfc4cbf45cd126f531dedda8485671545b43107ded25ce952aac6fb308",
|
||||||
|
"sha256:b7fbfabdbcc78c4f6fc4712544b9b0d6bf171069c6e0e3cb82440dd10ced3406",
|
||||||
|
"sha256:c05b9e4fb1d8a41d41dec8786c94f3b95d3c5f528298d769eb8e73d293abc48d",
|
||||||
|
"sha256:d7df6eddb6054d21ca4d3c6249cae5578cb4602951fd2b6ee2f5510ffb098707",
|
||||||
|
"sha256:e0b61738ab504e656d1fe4ff0c0601387a5489ca122d55390ade31f9ca0e252d",
|
||||||
|
"sha256:eff7d4a85e9eea55afa34888dfeaccde99e7520b51f867ac28a48492c0b1130c",
|
||||||
|
"sha256:f05644db6779387ccdb468cc47a44b4356fc2ffa9287135d05b70a98dc83b89a"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.782"
|
||||||
|
},
|
||||||
|
"mypy-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
|
||||||
|
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
|
||||||
|
],
|
||||||
|
"version": "==0.4.3"
|
||||||
|
},
|
||||||
|
"packaging": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
|
||||||
|
"sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
|
||||||
|
],
|
||||||
|
"version": "==20.4"
|
||||||
|
},
|
||||||
|
"pefile": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2019.4.18"
|
||||||
|
},
|
||||||
|
"psutil": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0ee3c36428f160d2d8fce3c583a0353e848abb7de9732c50cf3356dd49ad63f8",
|
||||||
|
"sha256:10512b46c95b02842c225f58fa00385c08fa00c68bac7da2d9a58ebe2c517498",
|
||||||
|
"sha256:4080869ed93cce662905b029a1770fe89c98787e543fa7347f075ade761b19d6",
|
||||||
|
"sha256:5e9d0f26d4194479a13d5f4b3798260c20cecf9ac9a461e718eb59ea520a360c",
|
||||||
|
"sha256:66c18ca7680a31bf16ee22b1d21b6397869dda8059dbdb57d9f27efa6615f195",
|
||||||
|
"sha256:68d36986ded5dac7c2dcd42f2682af1db80d4bce3faa126a6145c1637e1b559f",
|
||||||
|
"sha256:90990af1c3c67195c44c9a889184f84f5b2320dce3ee3acbd054e3ba0b4a7beb",
|
||||||
|
"sha256:a5b120bb3c0c71dfe27551f9da2f3209a8257a178ed6c628a819037a8df487f1",
|
||||||
|
"sha256:d8a82162f23c53b8525cf5f14a355f5d1eea86fa8edde27287dd3a98399e4fdf",
|
||||||
|
"sha256:f2018461733b23f308c298653c8903d32aaad7873d25e1d228765e91ae42c3f2",
|
||||||
|
"sha256:ff1977ba1a5f71f89166d5145c3da1cea89a0fdb044075a12c720ee9123ec818"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==5.7.2"
|
||||||
|
},
|
||||||
|
"pygments": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44",
|
||||||
|
"sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"
|
||||||
|
],
|
||||||
|
"version": "==2.6.1"
|
||||||
|
},
|
||||||
|
"pyinstaller": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3730fa80d088f8bb7084d32480eb87cbb4ddb64123363763cf8f2a1378c1c4b7"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.6"
|
||||||
|
},
|
||||||
|
"pylint": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7dd78437f2d8d019717dbf287772d0b2dbdfd13fc016aa7faa08d67bccc46adc",
|
||||||
|
"sha256:d0ece7d223fe422088b0e8f13fa0a1e8eb745ebffcb8ed53d3e95394b6101a1c"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.5.3"
|
||||||
|
},
|
||||||
|
"pyparsing": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
|
||||||
|
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
|
||||||
|
],
|
||||||
|
"version": "==2.4.7"
|
||||||
|
},
|
||||||
|
"pytz": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
|
||||||
|
"sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"
|
||||||
|
],
|
||||||
|
"version": "==2020.1"
|
||||||
|
},
|
||||||
|
"pywin32-ctypes": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942",
|
||||||
|
"sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.2.0"
|
||||||
|
},
|
||||||
|
"requests": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
|
||||||
|
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.24.0"
|
||||||
|
},
|
||||||
|
"six": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||||
|
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||||
|
],
|
||||||
|
"version": "==1.15.0"
|
||||||
|
},
|
||||||
|
"snowballstemmer": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:209f257d7533fdb3cb73bdbd24f436239ca3b2fa67d56f6ff88e86be08cc5ef0",
|
||||||
|
"sha256:df3bac3df4c2c01363f3dd2cfa78cce2840a79b9f1c2d2de9ce8d31683992f52"
|
||||||
|
],
|
||||||
|
"version": "==2.0.0"
|
||||||
|
},
|
||||||
|
"sphinx": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b4c750d546ab6d7e05bdff6ac24db8ae3e8b8253a3569b754e445110a0a12b66",
|
||||||
|
"sha256:fc312670b56cb54920d6cc2ced455a22a547910de10b3142276495ced49231cb"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.4.4"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-applehelp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a",
|
||||||
|
"sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"
|
||||||
|
],
|
||||||
|
"version": "==1.0.2"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-devhelp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e",
|
||||||
|
"sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"
|
||||||
|
],
|
||||||
|
"version": "==1.0.2"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-htmlhelp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3c0bc24a2c41e340ac37c85ced6dafc879ab485c095b1d65d2461ac2f7cca86f",
|
||||||
|
"sha256:e8f5bb7e31b2dbb25b9cc435c8ab7a79787ebf7f906155729338f3156d93659b"
|
||||||
|
],
|
||||||
|
"version": "==1.0.3"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-jsmath": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
|
||||||
|
"sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
|
||||||
|
],
|
||||||
|
"version": "==1.0.1"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-qthelp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72",
|
||||||
|
"sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"
|
||||||
|
],
|
||||||
|
"version": "==1.0.3"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-serializinghtml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:eaa0eccc86e982a9b939b2b82d12cc5d013385ba5eadcc7e4fed23f4405f77bc",
|
||||||
|
"sha256:f242a81d423f59617a8e5cf16f5d4d74e28ee9a66f9e5b637a18082991db5a9a"
|
||||||
|
],
|
||||||
|
"version": "==1.1.4"
|
||||||
|
},
|
||||||
|
"toml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
|
||||||
|
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
|
||||||
|
],
|
||||||
|
"version": "==0.10.1"
|
||||||
|
},
|
||||||
|
"typed-ast": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
|
||||||
|
"sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
|
||||||
|
"sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
|
||||||
|
"sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
|
||||||
|
"sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
|
||||||
|
"sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
|
||||||
|
"sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
|
||||||
|
"sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
|
||||||
|
"sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
|
||||||
|
"sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
|
||||||
|
"sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
|
||||||
|
"sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
|
||||||
|
"sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
|
||||||
|
"sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
|
||||||
|
"sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
|
||||||
|
"sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
|
||||||
|
"sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
|
||||||
|
"sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
|
||||||
|
"sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
|
||||||
|
"sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
|
||||||
|
"sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
|
||||||
|
],
|
||||||
|
"markers": "implementation_name == 'cpython' and python_version < '3.8'",
|
||||||
|
"version": "==1.4.1"
|
||||||
|
},
|
||||||
|
"typing-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5",
|
||||||
|
"sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae",
|
||||||
|
"sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392"
|
||||||
|
],
|
||||||
|
"version": "==3.7.4.2"
|
||||||
|
},
|
||||||
|
"urllib3": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
|
||||||
|
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
|
||||||
|
],
|
||||||
|
"version": "==1.25.10"
|
||||||
|
},
|
||||||
|
"wrapt": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b62ffa81fb85f4332a4f609cab4ac40709470da05643a082ec1eb88e6d9b97d7"
|
||||||
|
],
|
||||||
|
"version": "==1.12.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+87
-252
@@ -1,40 +1,70 @@
|
|||||||
Instaloader
|
.. image:: https://raw.githubusercontent.com/instaloader/instaloader/master/docs/logo_heading.png
|
||||||
===========
|
|
||||||
|
|
||||||
Download pictures (or videos) along with their captions and other metadata
|
.. badges-start
|
||||||
from Instagram.
|
|
||||||
|
|
||||||
Installation
|
|travis| |pypi| |pyversion| |license| |aur| |contributors| |downloads|
|
||||||
------------
|
|
||||||
|
|
||||||
Instaloader is written in Python, thus ensure having
|
.. |travis| image:: https://img.shields.io/travis/instaloader/instaloader/master.svg
|
||||||
`Python <https://www.python.org/>`__ (at least version 3.5) installed.
|
:alt: Travis-CI Build Status
|
||||||
|
:target: https://travis-ci.org/instaloader/instaloader
|
||||||
|
|
||||||
If you intend to use this tool under Windows, it is recommended
|
.. |pypi| image:: https://img.shields.io/pypi/v/instaloader.svg
|
||||||
to install
|
:alt: Instaloader PyPI Project Page
|
||||||
`win-unicode-console <https://pypi.python.org/pypi/win_unicode_console>`__.
|
:target: https://pypi.org/project/instaloader/
|
||||||
|
|
||||||
If you have `pip <https://pypi.python.org/pypi/pip>`__ installed, you
|
.. |license| image:: https://img.shields.io/github/license/instaloader/instaloader.svg
|
||||||
may install Instaloader using
|
:alt: MIT License
|
||||||
|
:target: https://github.com/instaloader/instaloader/blob/master/LICENSE
|
||||||
|
|
||||||
|
.. |pyversion| image:: https://img.shields.io/pypi/pyversions/instaloader.svg
|
||||||
|
:alt: Supported Python Versions
|
||||||
|
|
||||||
|
.. |contributors| image:: https://img.shields.io/github/contributors/instaloader/instaloader.svg
|
||||||
|
:alt: Contributor Count
|
||||||
|
:target: https://github.com/instaloader/instaloader/graphs/contributors
|
||||||
|
|
||||||
|
.. |aur| image:: https://img.shields.io/aur/version/instaloader.svg
|
||||||
|
:alt: Arch User Repository Package
|
||||||
|
:target: https://aur.archlinux.org/packages/instaloader/
|
||||||
|
|
||||||
|
.. |downloads| image:: https://pepy.tech/badge/instaloader/month
|
||||||
|
:alt: PyPI Download Count
|
||||||
|
:target: https://pepy.tech/project/instaloader/month
|
||||||
|
|
||||||
|
.. badges-end
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
pip3 install instaloader
|
$ pip3 install instaloader
|
||||||
|
|
||||||
Alternatively, to get the most current version of Instaloader from our
|
$ instaloader profile [profile ...]
|
||||||
`Git repository <https://github.com/Thammus/instaloader>`__:
|
|
||||||
|
**Instaloader**
|
||||||
|
|
||||||
|
- downloads **public and private profiles, hashtags, user stories,
|
||||||
|
feeds and saved media**,
|
||||||
|
|
||||||
|
- downloads **comments, geotags and captions** of each post,
|
||||||
|
|
||||||
|
- automatically **detects profile name changes** and renames the target
|
||||||
|
directory accordingly,
|
||||||
|
|
||||||
|
- allows **fine-grained customization** of filters and where to store
|
||||||
|
downloaded media,
|
||||||
|
|
||||||
|
- automatically **resumes previously-interrupted** download iterations.
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
pip3 install git+https://github.com/Thammus/instaloader
|
instaloader [--comments] [--geotags]
|
||||||
|
[--stories] [--highlights] [--tagged] [--igtv]
|
||||||
|
[--login YOUR-USERNAME] [--fast-update]
|
||||||
|
profile | "#hashtag" | :stories | :feed | :saved
|
||||||
|
|
||||||
(pass ``--upgrade`` to upgrade if Instaloader is already installed)
|
`Instaloader Documentation <https://instaloader.github.io/>`__
|
||||||
|
|
||||||
Instaloader requires
|
|
||||||
`requests <https://pypi.python.org/pypi/requests>`__, which
|
|
||||||
will be installed automatically, if it is not already installed.
|
|
||||||
|
|
||||||
How to automatically download pictures from Instagram
|
How to Automatically Download Pictures from Instagram
|
||||||
-----------------------------------------------------
|
-----------------------------------------------------
|
||||||
|
|
||||||
To **download all pictures and videos of a profile**, as well as the
|
To **download all pictures and videos of a profile**, as well as the
|
||||||
@@ -53,8 +83,10 @@ To later **update your local copy** of that profiles, you may run
|
|||||||
|
|
||||||
instaloader --fast-update profile [profile ...]
|
instaloader --fast-update profile [profile ...]
|
||||||
|
|
||||||
When ``--fast-update`` is given, Instaloader stops when arriving at
|
If ``--fast-update`` is given, Instaloader stops when arriving at the
|
||||||
the first already-downloaded picture.
|
first already-downloaded picture. When updating profiles, Instaloader
|
||||||
|
automatically **detects profile name changes** and renames the target directory
|
||||||
|
accordingly.
|
||||||
|
|
||||||
Instaloader can also be used to **download private profiles**. To do so,
|
Instaloader can also be used to **download private profiles**. To do so,
|
||||||
invoke it with
|
invoke it with
|
||||||
@@ -63,237 +95,40 @@ invoke it with
|
|||||||
|
|
||||||
instaloader --login=your_username profile [profile ...]
|
instaloader --login=your_username profile [profile ...]
|
||||||
|
|
||||||
When invoked like this, it also **stores the session cookies** in a file
|
When logging in, Instaloader **stores the session cookies** in a file in your
|
||||||
in your temporary directory, which will be reused later when ``--login`` is given. So
|
temporary directory, which will be reused later the next time ``--login``
|
||||||
you can download private profiles **non-interactively** when you already
|
is given. So you can download private profiles **non-interactively** when you
|
||||||
have a valid session cookie file.
|
already have a valid session cookie file.
|
||||||
|
|
||||||
Besides downloading private profiles, being logged in allows to
|
`Instaloader Documentation <https://instaloader.github.io/basic-usage.html>`__
|
||||||
**download stories**:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
instaloader --login=your_username --stories profile [profile ...]
|
|
||||||
|
|
||||||
You may also download
|
|
||||||
**the most recent pictures by hashtag**:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
instaloader "#hashtag"
|
|
||||||
|
|
||||||
This downloads the requested posts into a directory named according to the specified
|
|
||||||
hashtag and the filenames correspond to the timestamp of the posts.
|
|
||||||
As with all download tasks, this behavior can easily be customized, for example
|
|
||||||
encode the poster's profile name in the filenames:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
instaloader --filename-pattern={date}_{profile} "#hashtag"
|
|
||||||
|
|
||||||
If you want to **download all followees of a given profile**, call
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
instaloader --login=your_username @profile
|
|
||||||
|
|
||||||
To **download all the pictures from your feed which you have liked**, call
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
instaloader --login=your_username :feed-liked
|
|
||||||
|
|
||||||
or to **download all pictures from your feed**:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
instaloader --login=your_username :feed-all
|
|
||||||
|
|
||||||
**Download all stories** from the profiles you follow:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
instaloader --login=your_username --filename-pattern={date}_{profile} :stories
|
|
||||||
|
|
||||||
Advanced Options
|
|
||||||
----------------
|
|
||||||
|
|
||||||
The following flags can be given to Instaloader to specify how profiles should
|
|
||||||
be downloaded.
|
|
||||||
|
|
||||||
To get a list of all flags, their abbreviations and their descriptions, you may
|
|
||||||
run ``instaloader --help``.
|
|
||||||
|
|
||||||
What to Download
|
|
||||||
^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Specify a list of profiles or #hashtags. For each of these, Instaloader
|
|
||||||
creates a folder and downloads all posts along with the pictures's
|
|
||||||
captions and the current **profile picture**. If an already-downloaded profile
|
|
||||||
has been renamed, Instaloader automatically **finds it by its unique ID** and
|
|
||||||
renames the folder likewise.
|
|
||||||
|
|
||||||
Instead of a *profile* or a *#hashtag*, the special targets
|
|
||||||
``:feed-all`` (pictures from your feed),
|
|
||||||
``:feed-liked`` (pictures from your feed which you liked), and
|
|
||||||
``:stories`` (stories of your followees) can be specified.
|
|
||||||
|
|
||||||
--profile-pic-only Only download profile picture.
|
|
||||||
--skip-videos Do not download videos.
|
|
||||||
--geotags **Download geotags** when available. Geotags are stored as
|
|
||||||
a text file with the location's name and a Google Maps
|
|
||||||
link. This requires an additional request to the
|
|
||||||
Instagram server for each picture, which is why it is
|
|
||||||
disabled by default.
|
|
||||||
--comments Download and update comments for each post. This
|
|
||||||
requires an additional request to the Instagram server
|
|
||||||
for each post, which is why it is disabled by default.
|
|
||||||
--stories Also **download stories** of each profile that is
|
|
||||||
downloaded. Requires ``--login``.
|
|
||||||
--stories-only Rather than downloading regular posts of each
|
|
||||||
specified profile, only download stories.
|
|
||||||
Requires ``--login``.
|
|
||||||
|
|
||||||
When to Stop Downloading
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
If none of these options are given, Instaloader goes through all pictures
|
|
||||||
matching the specified targets.
|
|
||||||
|
|
||||||
--fast-update For each target, stop when encountering the first
|
|
||||||
already-downloaded picture. This flag is recommended
|
|
||||||
when you use Instaloader to update your personal
|
|
||||||
Instagram archive.
|
|
||||||
--count COUNT Do not attempt to download more than COUNT posts.
|
|
||||||
Applies only to ``#hashtag``, ``:feed-all`` and ``:feed-liked``.
|
|
||||||
|
|
||||||
|
|
||||||
Login (Download Private Profiles)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
Instaloader can **login to Instagram**. This allows downloading private
|
|
||||||
profiles. To login, pass the ``--login`` option. Your session cookie (not your
|
|
||||||
password!) will be saved to a local file to be reused next time you want
|
|
||||||
Instaloader to login.
|
|
||||||
|
|
||||||
--login YOUR-USERNAME Login name (profile name) for your Instagram account.
|
|
||||||
--sessionfile SESSIONFILE Path for loading and storing session key file.
|
|
||||||
Defaults to a path
|
|
||||||
within your temporary directory, encoding your local
|
|
||||||
username and your Instagram profile name.
|
|
||||||
--password YOUR-PASSWORD Password for your Instagram account. Without this
|
|
||||||
option, you'll be prompted for your password
|
|
||||||
interactively if there is not yet a valid session
|
|
||||||
file.
|
|
||||||
|
|
||||||
How to Download
|
|
||||||
^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
--dirname-pattern DIRNAME_PATTERN
|
|
||||||
Name of directory where to store posts. ``{profile}``
|
|
||||||
is replaced by the profile name, ``{target}`` is replaced
|
|
||||||
by the target you specified, i.e. either ``:feed``,
|
|
||||||
``#hashtag`` or the profile name. Defaults to ``{target}``.
|
|
||||||
--filename-pattern FILENAME_PATTERN
|
|
||||||
Prefix of filenames. Posts are stored in the
|
|
||||||
directory whose pattern is given with ``--dirname-pattern``.
|
|
||||||
``{profile}`` is replaced by the profile name,
|
|
||||||
``{target}`` is replaced by the target you specified, i.e.
|
|
||||||
either ``:feed``, ``#hashtag`` or the profile name. Also, the
|
|
||||||
fields ``{date}`` and ``{shortcode}`` can be specified.
|
|
||||||
Defaults to ``{date:%Y-%m-%d_%H-%M-%S}``.
|
|
||||||
--user-agent USER_AGENT User Agent to use for HTTP requests. Per default,
|
|
||||||
Instaloader pretends being Chrome/51.
|
|
||||||
--no-sleep Do not sleep between requests to Instagram's servers.
|
|
||||||
This makes downloading faster, but may be suspicious.
|
|
||||||
|
|
||||||
Miscellaneous Options
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
--shorter-output Do not display captions while downloading.
|
|
||||||
--quiet Disable user interaction, i.e. do not print messages
|
|
||||||
(except errors) and fail if login credentials are
|
|
||||||
needed but not given. This makes Instaloader
|
|
||||||
**suitable as a cron job**.
|
|
||||||
|
|
||||||
Usage as Python module
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
You may also use parts of Instaloader as library to do other interesting
|
|
||||||
things.
|
|
||||||
|
|
||||||
For example, to get a list of all followees and a list of all followers of a profile, do
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
import instaloader
|
|
||||||
|
|
||||||
# Get instance
|
|
||||||
loader = instaloader.Instaloader()
|
|
||||||
|
|
||||||
# Login
|
|
||||||
loader.interactive_login(USERNAME)
|
|
||||||
|
|
||||||
# Retrieve followees
|
|
||||||
followees = loader.get_followees(PROFILE)
|
|
||||||
print(PROFILE + " follows these profiles:")
|
|
||||||
for f in followees:
|
|
||||||
print("\t%s\t%s" % (f['username'], f['full_name']))
|
|
||||||
|
|
||||||
# Retrieve followers
|
|
||||||
followers = loader.get_followers(PROFILE)
|
|
||||||
print("Followers of " + PROFILE + ":")
|
|
||||||
for f in followers:
|
|
||||||
print("\t%s\t%s" % (f['username'], f['full_name']))
|
|
||||||
|
|
||||||
Then, you may download all pictures of all followees with
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
for f in followees:
|
|
||||||
try:
|
|
||||||
loader.download(f['username'])
|
|
||||||
except instaloader.NonfatalException:
|
|
||||||
pass
|
|
||||||
|
|
||||||
You could also download your last 20 liked pics with
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
loader.download_feed_pics(max_count=20, fast_update=True,
|
|
||||||
filter_func=lambda node:
|
|
||||||
not node["likes"]["viewer_has_liked"] if "likes" in node else not node["viewer_has_liked"])
|
|
||||||
|
|
||||||
To download the last 20 pictures with hashtag #cat, do
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
loader.download_hashtag('cat', max_count=20)
|
|
||||||
|
|
||||||
If logged in, Instaloader is also able to download user stories:
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
loader.download_stories()
|
|
||||||
|
|
||||||
Each Instagram profile has its own unique ID which stays unmodified even
|
|
||||||
if a user changes his/her username. To get said ID, given the profile's
|
|
||||||
name, you may call
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
loader.get_id_by_username(PROFILE_NAME)
|
|
||||||
|
|
||||||
``get_followees()`` also returns unique IDs for all loaded followees. To
|
|
||||||
get the current username of a profile, given this unique ID
|
|
||||||
``get_username_by_id()`` can be used. For example:
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
loader.get_username_by_id(followees[0]['id'])
|
|
||||||
|
|
||||||
Disclaimer
|
Disclaimer
|
||||||
----------
|
----------
|
||||||
|
|
||||||
This code is in no way affiliated with, authorized, maintained or endorsed by Instagram or any of its affiliates or
|
.. disclaimer-start
|
||||||
subsidiaries. This is an independent and unofficial project. Use at your own risk.
|
|
||||||
|
Instaloader is in no way affiliated with, authorized, maintained or endorsed by Instagram or any of its affiliates or
|
||||||
|
subsidiaries. This is an independent and unofficial project. Use at your own risk.
|
||||||
|
|
||||||
|
Instaloader is licensed under an MIT license. Refer to ``LICENSE`` file for more information.
|
||||||
|
|
||||||
|
.. disclaimer-end
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
------------
|
||||||
|
|
||||||
|
As an open source project, Instaloader heavily depends on the contributions from
|
||||||
|
its community. See
|
||||||
|
`contributing <https://instaloader.github.io/contributing.html>`__
|
||||||
|
for how you may help Instaloader to become an even greater tool.
|
||||||
|
|
||||||
|
It is a pleasure for us to share our Instaloader to the world, and we are proud
|
||||||
|
to have attracted such an active and motivating community, with so many users
|
||||||
|
who share their suggestions and ideas with us. Buying a community-sponsored beer
|
||||||
|
or coffee from time to time is very likely to further raise our passion for the
|
||||||
|
development of Instaloader.
|
||||||
|
|
||||||
|
| For Donations, we provide a PayPal.Me link and a Bitcoin address.
|
||||||
|
| PayPal: `PayPal.me/aandergr <https://www.paypal.me/aandergr>`__
|
||||||
|
| BTC: 1Nst4LoadeYzrKjJ1DX9CpbLXBYE9RKLwY
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
pkgbase = instaloader
|
||||||
|
pkgdesc = Command line tool to download pictures, videos and metadata from Instagram
|
||||||
|
pkgver = {{version}}
|
||||||
|
pkgrel = 1
|
||||||
|
url = https://instaloader.github.io/
|
||||||
|
arch = any
|
||||||
|
license = MIT
|
||||||
|
makedepends = python-setuptools
|
||||||
|
depends = python>=3.5
|
||||||
|
depends = python-requests>=2.4
|
||||||
|
options = !emptydirs
|
||||||
|
source = instaloader-{{version}}.tar.gz::https://codeload.github.com/instaloader/instaloader/tar.gz/v{{version}}
|
||||||
|
sha512sums = {{hash}}
|
||||||
|
|
||||||
|
pkgname = instaloader
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Maintainer: André Koch-Kramer <koch-kramer@web.de>
|
||||||
|
|
||||||
|
pkgname=instaloader
|
||||||
|
pkgver={{version}}
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="Command line tool to download pictures, videos and metadata from Instagram"
|
||||||
|
arch=('any')
|
||||||
|
url="https://instaloader.github.io/"
|
||||||
|
license=('MIT')
|
||||||
|
groups=()
|
||||||
|
depends=('python>=3.5' 'python-requests>=2.4')
|
||||||
|
makedepends=('python-setuptools')
|
||||||
|
options=('!emptydirs')
|
||||||
|
source=($pkgname-$pkgver.tar.gz::https://codeload.github.com/instaloader/instaloader/tar.gz/v$pkgver)
|
||||||
|
sha512sums=('{{hash}}')
|
||||||
|
|
||||||
|
build() {
|
||||||
|
cd "$srcdir/$pkgname-$pkgver"
|
||||||
|
python setup.py clean --all
|
||||||
|
python setup.py build
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "$srcdir/$pkgname-$pkgver"
|
||||||
|
install -Dm644 LICENSE "$pkgdir"/usr/share/licenses/$pkgname/LICENSE
|
||||||
|
python setup.py install --root "$pkgdir/" --skip-build
|
||||||
|
}
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
VERSION=${1:1}
|
||||||
|
|
||||||
|
# do not deploy pre-releases
|
||||||
|
echo $VERSION | grep -qe "[abc]" && exit 0
|
||||||
|
|
||||||
|
cd $(dirname $0)
|
||||||
|
|
||||||
|
# decrypt and add ssh key
|
||||||
|
openssl aes-256-cbc -K $encrypted_354637631c28_key -iv $encrypted_354637631c28_iv -in id_rsa_AUR.enc -out /tmp/AUR_openssh -d
|
||||||
|
eval "$(ssh-agent -s)"
|
||||||
|
chmod 600 /tmp/AUR_openssh
|
||||||
|
ssh-add /tmp/AUR_openssh
|
||||||
|
|
||||||
|
# clone and modify AUR repo
|
||||||
|
git clone --depth 1 ssh://aur@aur.archlinux.org/instaloader.git
|
||||||
|
|
||||||
|
curl -sSfOJ https://codeload.github.com/instaloader/instaloader/tar.gz/$1
|
||||||
|
HASH=$(sha512sum instaloader-$VERSION.tar.gz | cut -f1 -d " ")
|
||||||
|
sed -e "s/{{version}}/$VERSION/g" -e "s/{{hash}}/$HASH/g" PKGBUILD.template > instaloader/PKGBUILD
|
||||||
|
sed -e "s/{{version}}/$VERSION/g" -e "s/{{hash}}/$HASH/g" .SRCINFO.template > instaloader/.SRCINFO
|
||||||
|
|
||||||
|
# commit and push changes
|
||||||
|
cd instaloader
|
||||||
|
git config user.email "koch-kramer@web.de"
|
||||||
|
git config user.name "André Koch-Kramer"
|
||||||
|
git add .
|
||||||
|
git commit -m "Release of version $VERSION"
|
||||||
|
git push
|
||||||
Binary file not shown.
@@ -0,0 +1,71 @@
|
|||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# copy the required files into repo root
|
||||||
|
shutil.copy('docs/favicon.ico', '.')
|
||||||
|
shutil.copy('deploy/windows/instaloader.spec', '.')
|
||||||
|
|
||||||
|
code = """
|
||||||
|
import contextlib
|
||||||
|
import psutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
def __main():
|
||||||
|
with contextlib.suppress(AttributeError, psutil.Error):
|
||||||
|
if psutil.Process().parent().parent().name() == "explorer.exe":
|
||||||
|
subprocess.Popen("powershell -NoExit -Command \\\"& '{0}'\\\"".format(sys.argv[0]))
|
||||||
|
return
|
||||||
|
main()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
__main()
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open('instaloader/__main__.py', 'r') as f:
|
||||||
|
# adjust imports for changed file structure
|
||||||
|
regex = re.compile(r'from (?:(\.[^ ]+ )|\.( ))import')
|
||||||
|
lines = [regex.sub(r'from instaloader\1\2import', line) for line in f.readlines()]
|
||||||
|
|
||||||
|
# insert code for magic exe behavior
|
||||||
|
index = lines.index('if __name__ == "__main__":\n')
|
||||||
|
code_lines = [cl + '\n' for cl in code.splitlines()]
|
||||||
|
for i, code_line in enumerate(code_lines):
|
||||||
|
if i + index < len(lines):
|
||||||
|
lines[i + index] = code_line
|
||||||
|
else:
|
||||||
|
lines.extend(code_lines[i:])
|
||||||
|
break
|
||||||
|
|
||||||
|
with open('__main__.py', 'w+') as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
|
||||||
|
# install dependencies and invoke PyInstaller
|
||||||
|
commands = ["pip install pipenv==2020.6.2",
|
||||||
|
"pipenv sync --dev",
|
||||||
|
"pipenv run pyinstaller --log-level=DEBUG instaloader.spec"]
|
||||||
|
|
||||||
|
for command in commands:
|
||||||
|
print()
|
||||||
|
print('#' * (len(command) + 6))
|
||||||
|
print('## {} ##'.format(command))
|
||||||
|
print('#' * (len(command) + 6))
|
||||||
|
print(flush=True)
|
||||||
|
err = subprocess.Popen(command).wait()
|
||||||
|
if err != 0:
|
||||||
|
sys.exit(err)
|
||||||
|
|
||||||
|
# calculate and store MD5 hash for created executable
|
||||||
|
hash_md5 = hashlib.md5()
|
||||||
|
with open('dist/instaloader.exe', 'rb') as f:
|
||||||
|
for chunk in iter(lambda: f.read(4096), b''):
|
||||||
|
hash_md5.update(chunk)
|
||||||
|
|
||||||
|
with open('dist/instaloader.exe.md5', 'w+') as f:
|
||||||
|
f.write('{} *instaloader.exe\n'.format(hash_md5.hexdigest()))
|
||||||
|
|
||||||
|
# Create ZIP file
|
||||||
|
shutil.make_archive('instaloader-{}-windows-standalone'.format(os.getenv('VERSION_TAG')), 'zip', 'dist')
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# -*- mode: python -*-
|
||||||
|
|
||||||
|
block_cipher = None
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(['__main__.py'],
|
||||||
|
binaries=[],
|
||||||
|
datas=[],
|
||||||
|
hiddenimports=[],
|
||||||
|
hookspath=[],
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
win_no_prefer_redirects=False,
|
||||||
|
win_private_assemblies=False,
|
||||||
|
cipher=block_cipher)
|
||||||
|
pyz = PYZ(a.pure, a.zipped_data,
|
||||||
|
cipher=block_cipher)
|
||||||
|
exe = EXE(pyz,
|
||||||
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.zipfiles,
|
||||||
|
a.datas,
|
||||||
|
name='instaloader',
|
||||||
|
debug=False,
|
||||||
|
strip=False,
|
||||||
|
upx=False,
|
||||||
|
runtime_tmpdir=None,
|
||||||
|
console=True , icon='favicon.ico')
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
_build/
|
||||||
+231
@@ -0,0 +1,231 @@
|
|||||||
|
# Makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
PAPER =
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# Internal variables.
|
||||||
|
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||||
|
PAPEROPT_letter = -D latex_paper_size=letter
|
||||||
|
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
# the i18n builder cannot share the environment and doctrees with the others
|
||||||
|
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
|
||||||
|
.PHONY: help
|
||||||
|
help:
|
||||||
|
@echo "Please use \`make <target>' where <target> is one of"
|
||||||
|
@echo " html to make standalone HTML files"
|
||||||
|
@echo " dirhtml to make HTML files named index.html in directories"
|
||||||
|
@echo " singlehtml to make a single large HTML file"
|
||||||
|
@echo " pickle to make pickle files"
|
||||||
|
@echo " json to make JSON files"
|
||||||
|
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||||
|
@echo " qthelp to make HTML files and a qthelp project"
|
||||||
|
@echo " applehelp to make an Apple Help Book"
|
||||||
|
@echo " devhelp to make HTML files and a Devhelp project"
|
||||||
|
@echo " epub to make an epub"
|
||||||
|
@echo " epub3 to make an epub3"
|
||||||
|
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||||
|
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||||
|
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
|
||||||
|
@echo " text to make text files"
|
||||||
|
@echo " man to make manual pages"
|
||||||
|
@echo " texinfo to make Texinfo files"
|
||||||
|
@echo " info to make Texinfo files and run them through makeinfo"
|
||||||
|
@echo " gettext to make PO message catalogs"
|
||||||
|
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||||
|
@echo " xml to make Docutils-native XML files"
|
||||||
|
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
|
||||||
|
@echo " linkcheck to check all external links for integrity"
|
||||||
|
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||||
|
@echo " coverage to run coverage check of the documentation (if enabled)"
|
||||||
|
@echo " dummy to check syntax errors of document sources"
|
||||||
|
|
||||||
|
.PHONY: clean
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILDDIR)/*
|
||||||
|
|
||||||
|
.PHONY: html
|
||||||
|
html:
|
||||||
|
LC_ALL=C $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||||
|
|
||||||
|
.PHONY: dirhtml
|
||||||
|
dirhtml:
|
||||||
|
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||||
|
|
||||||
|
.PHONY: singlehtml
|
||||||
|
singlehtml:
|
||||||
|
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||||
|
|
||||||
|
.PHONY: pickle
|
||||||
|
pickle:
|
||||||
|
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the pickle files."
|
||||||
|
|
||||||
|
.PHONY: json
|
||||||
|
json:
|
||||||
|
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the JSON files."
|
||||||
|
|
||||||
|
.PHONY: htmlhelp
|
||||||
|
htmlhelp:
|
||||||
|
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||||
|
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||||
|
|
||||||
|
.PHONY: qthelp
|
||||||
|
qthelp:
|
||||||
|
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||||
|
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||||
|
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Instaloader.qhcp"
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Instaloader.qhc"
|
||||||
|
|
||||||
|
.PHONY: applehelp
|
||||||
|
applehelp:
|
||||||
|
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
|
||||||
|
@echo "N.B. You won't be able to view it unless you put it in" \
|
||||||
|
"~/Library/Documentation/Help or install it in your application" \
|
||||||
|
"bundle."
|
||||||
|
|
||||||
|
.PHONY: devhelp
|
||||||
|
devhelp:
|
||||||
|
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished."
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# mkdir -p $$HOME/.local/share/devhelp/Instaloader"
|
||||||
|
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Instaloader"
|
||||||
|
@echo "# devhelp"
|
||||||
|
|
||||||
|
.PHONY: epub
|
||||||
|
epub:
|
||||||
|
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||||
|
|
||||||
|
.PHONY: epub3
|
||||||
|
epub3:
|
||||||
|
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
|
||||||
|
|
||||||
|
.PHONY: latex
|
||||||
|
latex:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||||
|
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||||
|
"(use \`make latexpdf' here to do that automatically)."
|
||||||
|
|
||||||
|
.PHONY: latexpdf
|
||||||
|
latexpdf:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo "Running LaTeX files through pdflatex..."
|
||||||
|
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||||
|
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||||
|
|
||||||
|
.PHONY: latexpdfja
|
||||||
|
latexpdfja:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo "Running LaTeX files through platex and dvipdfmx..."
|
||||||
|
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
|
||||||
|
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||||
|
|
||||||
|
.PHONY: text
|
||||||
|
text:
|
||||||
|
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||||
|
|
||||||
|
.PHONY: man
|
||||||
|
man:
|
||||||
|
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||||
|
|
||||||
|
.PHONY: texinfo
|
||||||
|
texinfo:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||||
|
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||||
|
"(use \`make info' here to do that automatically)."
|
||||||
|
|
||||||
|
.PHONY: info
|
||||||
|
info:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo "Running Texinfo files through makeinfo..."
|
||||||
|
make -C $(BUILDDIR)/texinfo info
|
||||||
|
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||||
|
|
||||||
|
.PHONY: gettext
|
||||||
|
gettext:
|
||||||
|
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||||
|
|
||||||
|
.PHONY: changes
|
||||||
|
changes:
|
||||||
|
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||||
|
@echo
|
||||||
|
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||||
|
|
||||||
|
.PHONY: linkcheck
|
||||||
|
linkcheck:
|
||||||
|
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||||
|
@echo
|
||||||
|
@echo "Link check complete; look for any errors in the above output " \
|
||||||
|
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||||
|
|
||||||
|
.PHONY: doctest
|
||||||
|
doctest:
|
||||||
|
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||||
|
@echo "Testing of doctests in the sources finished, look at the " \
|
||||||
|
"results in $(BUILDDIR)/doctest/output.txt."
|
||||||
|
|
||||||
|
.PHONY: coverage
|
||||||
|
coverage:
|
||||||
|
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
|
||||||
|
@echo "Testing of coverage in the sources finished, look at the " \
|
||||||
|
"results in $(BUILDDIR)/coverage/python.txt."
|
||||||
|
|
||||||
|
.PHONY: xml
|
||||||
|
xml:
|
||||||
|
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
|
||||||
|
|
||||||
|
.PHONY: pseudoxml
|
||||||
|
pseudoxml:
|
||||||
|
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
|
||||||
|
|
||||||
|
.PHONY: dummy
|
||||||
|
dummy:
|
||||||
|
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. Dummy builder generates no files."
|
||||||
|
|
||||||
|
favicon.ico: logo.svg
|
||||||
|
convert -density 384 -background transparent -define icon:auto-resize -colors 256 logo.svg favicon.ico
|
||||||
|
|
||||||
|
logo.png: logo.svg
|
||||||
|
inkscape -z -e logo.png -w 100 -h 100 logo.svg
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Instaloader Documentation
|
||||||
|
|
||||||
|
This directory contains sources for the Instaloader documentation.
|
||||||
|
|
||||||
|
The documentation is hosted at:
|
||||||
|
https://instaloader.github.io/
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
The documentation is created with [Sphinx](http://www.sphinx-doc.org/). To build it, use
|
||||||
|
|
||||||
|
```
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
make html
|
||||||
|
```
|
||||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+50
File diff suppressed because one or more lines are too long
Vendored
+83
@@ -0,0 +1,83 @@
|
|||||||
|
{% extends "!layout.html" %}
|
||||||
|
{% set html5_doctype = True %}
|
||||||
|
{% set script_files = script_files + ['_static/instaloaderdoc.js'] %}
|
||||||
|
{% block htmltitle %}
|
||||||
|
{% if pagename == "index" %}
|
||||||
|
<title>{{ title|striptags|e }}</title>
|
||||||
|
{% else %}
|
||||||
|
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block extrahead %}
|
||||||
|
{{ super() }}
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
<meta name="google-site-verification" content="FNUsDdkOJM9obm9QEdrNBVd9AtAzHzA-aaSMCUrS6C8"/>
|
||||||
|
{% if pagename == "index" %}
|
||||||
|
<link rel="canonical" href="https://instaloader.github.io/"/>
|
||||||
|
{% else %}
|
||||||
|
<link rel="canonical" href="https://instaloader.github.io/{{ pagename }}.html"/>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block header %}
|
||||||
|
<nav class="navbar navbar-light navbar-expand bg-light border-bottom">
|
||||||
|
<a class="navbar-brand" href="{{ pathto(master_doc) }}">
|
||||||
|
<img src="{{ pathto('_static/' + logo, 1) }}" width="30" height="30" class="d-inline-block align-top" alt="logo">
|
||||||
|
<span class="ml-3">Instaloader</span>
|
||||||
|
</a>
|
||||||
|
<ul class="navbar-nav mr-auto">
|
||||||
|
<li class="nav-item ml-0 ml-sm-2">
|
||||||
|
<a class="nav-link"
|
||||||
|
href="https://github.com/instaloader/instaloader/releases/tag/v{{ current_release }}"
|
||||||
|
title="Released on {{ current_release_date }}" data-toggle="tooltip">
|
||||||
|
v<b>{{ current_release }}</b>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav ml-auto d-none d-sm-flex">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://github.com/instaloader/instaloader/issues">
|
||||||
|
Issues
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://github.com/instaloader/instaloader/releases">
|
||||||
|
Releases
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://github.com/instaloader/instaloader">
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button class="d-inline-block d-md-none btn btn-outline-primary btn-sm" type="button"
|
||||||
|
data-toggle="collapse" data-target="#instaloaderdocSidebar" aria-controls="instaloaderdocSidebar"
|
||||||
|
aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
{% endblock %}
|
||||||
|
{% block relbar1 %}{% endblock %}
|
||||||
|
{% block relbar2 %}{% endblock %}
|
||||||
|
{% block footer %}{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row">
|
||||||
|
<div class="instaloaderdoc-sidebar collapse d-md-block col-md-3 col-xl-2 bg-light"
|
||||||
|
id="instaloaderdocSidebar">
|
||||||
|
<nav role="navigation" aria-label="Table of Contents">
|
||||||
|
<form action="{{ pathto('search') }}" method="get" role="search" class="border-bottom mr-2 pb-2 mb-2 px-1">
|
||||||
|
<label class="sr-only" for="searchQueryInput">Search</label>
|
||||||
|
<input type="search" name="q" id="searchQueryInput" class="form-control my-2"
|
||||||
|
placeholder="Search..." aria-label="Search">
|
||||||
|
</form>
|
||||||
|
<h4>Contents</h4>
|
||||||
|
{{ toctree(maxdepth=-1) }}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<main class="col-12 col-md-9 col-xl-10 pt-2" role="main">
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
{% extends "!page.html" %}
|
||||||
|
{% block body %}
|
||||||
|
{{ super() }}
|
||||||
|
{% if next and prev %}
|
||||||
|
<h2>Next Section</h2>
|
||||||
|
<p>
|
||||||
|
<a class="reference internal" href="{{ next.link|e }}">{{ next.title }}</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
Vendored
+28
@@ -0,0 +1,28 @@
|
|||||||
|
{% extends "!search.html" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<div id="fallback" class="admonition warning">
|
||||||
|
<script type="text/javascript">$('#fallback').hide();</script>
|
||||||
|
<p>
|
||||||
|
{% trans %}Please activate JavaScript to enable the search
|
||||||
|
functionality.{% endtrans %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% if search_performed %}
|
||||||
|
<h1>{{ _('Search Results') }}</h1>
|
||||||
|
{% if not search_results %}
|
||||||
|
<p>{{ _('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\'ve selected enough categories.') }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<div id="search-results">
|
||||||
|
{% if search_results %}
|
||||||
|
<ul>
|
||||||
|
{% for href, caption, context in search_results %}
|
||||||
|
<li><a href="{{ pathto(item.href) }}">{{ caption }}</a>
|
||||||
|
<div class="context">{{ context|e }}</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
.. meta::
|
||||||
|
:description:
|
||||||
|
Documentation of Instaloader module, a powerful and intuitive
|
||||||
|
Python library to download Instagram media and metadata.
|
||||||
|
|
||||||
|
.. _python-module-instaloader:
|
||||||
|
|
||||||
|
Python Module :mod:`instaloader`
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
.. module:: instaloader
|
||||||
|
|
||||||
|
.. highlight:: python
|
||||||
|
|
||||||
|
Instaloader exposes its internally used methods and structures as a Python
|
||||||
|
module, making it a powerful and intuitive Python API for Instagram,
|
||||||
|
allowing to further customize obtaining media and metadata.
|
||||||
|
|
||||||
|
Start with getting an instance of :class:`Instaloader`::
|
||||||
|
|
||||||
|
import instaloader
|
||||||
|
|
||||||
|
# Get instance
|
||||||
|
L = instaloader.Instaloader()
|
||||||
|
|
||||||
|
# Optionally, login or load session
|
||||||
|
L.login(USER, PASSWORD) # (login)
|
||||||
|
L.interactive_login(USER) # (ask password on terminal)
|
||||||
|
L.load_session_from_file(USER) # (load session created w/
|
||||||
|
# `instaloader -l USERNAME`)
|
||||||
|
|
||||||
|
:mod:`instaloader` provides the :class:`Post` structure, which represents a
|
||||||
|
picture, video or sidecar (set of multiple pictures/videos) posted in a user's
|
||||||
|
profile. :class:`Instaloader` provides methods to iterate over Posts from a
|
||||||
|
certain source::
|
||||||
|
|
||||||
|
for post in instaloader.Hashtag.from_name(L.context, 'cat').get_posts():
|
||||||
|
# post is an instance of instaloader.Post
|
||||||
|
L.download_post(post, target='#cat')
|
||||||
|
|
||||||
|
:class:`Post` instances can be created with:
|
||||||
|
|
||||||
|
- :func:`Post.from_shortcode`
|
||||||
|
Use a Post shortcode (part of the Post URL,
|
||||||
|
``https://www.instagram.com/p/SHORTCODE/``) to create a Post
|
||||||
|
object::
|
||||||
|
|
||||||
|
post = Post.from_shortcode(L.context, SHORTCODE)
|
||||||
|
|
||||||
|
- :meth:`Profile.get_posts`
|
||||||
|
All media of a :class:`Profile`.
|
||||||
|
|
||||||
|
- :meth:`Profile.get_saved_posts`
|
||||||
|
Media that the user marked as saved (:class:`Profile` must be own profile
|
||||||
|
for this to work)
|
||||||
|
|
||||||
|
- :meth:`Instaloader.get_feed_posts`
|
||||||
|
Media in the user's feed.
|
||||||
|
|
||||||
|
- :meth:`Instaloader.get_explore_posts`
|
||||||
|
Media that is suggested by Instagram to explore.
|
||||||
|
|
||||||
|
- :meth:`Hashtag.get_posts`
|
||||||
|
Media associated with given hashtag.
|
||||||
|
|
||||||
|
With the :class:`Profile` class, Instaloader also makes it easy to access
|
||||||
|
metadata of a Profile. :class:`Profile` instances can be created with:
|
||||||
|
|
||||||
|
- :meth:`Profile.from_username`::
|
||||||
|
|
||||||
|
profile = Profile.from_username(L.context, USERNAME)
|
||||||
|
|
||||||
|
- :meth:`Profile.from_id`
|
||||||
|
given its User ID. This allows to easily lookup a Profile's username given
|
||||||
|
its ID::
|
||||||
|
|
||||||
|
Profile.from_id(L.context, USERID).username
|
||||||
|
|
||||||
|
- :meth:`Profile.get_followees`
|
||||||
|
Profiles that are followed by given user.
|
||||||
|
|
||||||
|
- :meth:`Profile.get_followers`
|
||||||
|
Profiles that follow given user.
|
||||||
|
|
||||||
|
- :attr:`Post.owner_profile`, :attr:`Story.owner_profile`, :attr:`StoryItem.owner_profile`
|
||||||
|
Owner profile of particular object.
|
||||||
|
|
||||||
|
- :meth:`Post.get_likes`
|
||||||
|
Profiles that liked a given :class:`Post`
|
||||||
|
|
||||||
|
- :attr:`PostComment.owner`
|
||||||
|
Profile of a Post comment.
|
||||||
|
|
||||||
|
For a list of a few code examples that use the Instaloader module, see
|
||||||
|
:ref:`codesnippets`.
|
||||||
|
|
||||||
|
The reference of the classes and functions provided by the :mod:`instaloader` module is
|
||||||
|
divided into the following subsections:
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
module/instaloader
|
||||||
|
module/structures
|
||||||
|
module/nodeiterator
|
||||||
|
module/instaloadercontext
|
||||||
|
module/exceptions
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
.. meta::
|
||||||
|
:description:
|
||||||
|
How to download pictures from Instagram. Description of basic
|
||||||
|
usage of Instaloader, free tool to download photos from public
|
||||||
|
and private profiles, hashtags, stories, feeds, saved media, and
|
||||||
|
their metadata, comments and captions.
|
||||||
|
|
||||||
|
.. _download-pictures-from-instagram:
|
||||||
|
|
||||||
|
Download Pictures from Instagram
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
.. highlight:: none
|
||||||
|
|
||||||
|
Here we describe how to use Instaloader to download pictures from Instagram. If
|
||||||
|
you do not have Instaloader installed yet, see :ref:`install`.
|
||||||
|
|
||||||
|
.. NOTE that Section "Basic Usage" is duplicated in README.rst.
|
||||||
|
|
||||||
|
Basic Usage
|
||||||
|
^^^^^^^^^^^
|
||||||
|
|
||||||
|
To **download all pictures and videos of a profile**, as well as the
|
||||||
|
**profile picture**, do
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
instaloader profile [profile ...]
|
||||||
|
|
||||||
|
where ``profile`` is the name of a profile you want to download. Instead
|
||||||
|
of only one profile, you may also specify a list of profiles.
|
||||||
|
|
||||||
|
To later **update your local copy** of that profiles, you may run
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
instaloader --fast-update profile [profile ...]
|
||||||
|
|
||||||
|
If :option:`--fast-update` is given, Instaloader stops when arriving at the
|
||||||
|
first already-downloaded picture.
|
||||||
|
|
||||||
|
When updating profiles, Instaloader
|
||||||
|
automatically **detects profile name changes** and renames the target directory
|
||||||
|
accordingly.
|
||||||
|
|
||||||
|
Instaloader can also be used to **download private profiles**. To do so,
|
||||||
|
invoke it with
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
instaloader --login=your_username profile [profile ...]
|
||||||
|
|
||||||
|
When logging in, Instaloader **stores the session cookies** in a file in your
|
||||||
|
home directory, which will be reused later the next time :option:`--login`
|
||||||
|
is given. So you can download private profiles **non-interactively** when you
|
||||||
|
already have a valid session cookie file.
|
||||||
|
|
||||||
|
.. _what-to-download:
|
||||||
|
|
||||||
|
What to Download
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. targets-start
|
||||||
|
|
||||||
|
Instaloader supports the following targets:
|
||||||
|
|
||||||
|
- ``profile``
|
||||||
|
Public profile, or private profile with :option:`--login`.
|
||||||
|
|
||||||
|
If an already-downloaded profile has been renamed, Instaloader automatically
|
||||||
|
finds it by its unique ID and renames the folder accordingly.
|
||||||
|
|
||||||
|
Besides the profile's posts, its current profile picture is downloaded. For
|
||||||
|
each profile you download,
|
||||||
|
|
||||||
|
- :option:`--stories`
|
||||||
|
instructs Instaloader to also **download the user's stories**,
|
||||||
|
|
||||||
|
- :option:`--highlights`
|
||||||
|
to **download the highlights of that profile**,
|
||||||
|
|
||||||
|
- :option:`--tagged`
|
||||||
|
to **download posts where the user is tagged**, and
|
||||||
|
|
||||||
|
- :option:`--igtv`
|
||||||
|
to **download IGTV videos**.
|
||||||
|
|
||||||
|
- ``"#hashtag"``
|
||||||
|
Posts with a certain **hashtag** (the quotes are usually necessary).
|
||||||
|
|
||||||
|
- ``%location id``
|
||||||
|
Posts tagged with a given location; the location ID is the numerical ID
|
||||||
|
Instagram labels a location with (e.g.
|
||||||
|
\https://www.instagram.com/explore/locations/**362629379**/plymouth-naval-memorial/).
|
||||||
|
Requires :option:`--login`.
|
||||||
|
|
||||||
|
.. versionadded:: 4.2
|
||||||
|
|
||||||
|
- ``:stories``
|
||||||
|
The currently-visible **stories** of your followees (requires
|
||||||
|
:option:`--login`).
|
||||||
|
|
||||||
|
- ``:feed``
|
||||||
|
Your **feed** (requires :option:`--login`).
|
||||||
|
|
||||||
|
- ``:saved``
|
||||||
|
Posts which are marked as **saved** (requires :option:`--login`).
|
||||||
|
|
||||||
|
- ``@profile``
|
||||||
|
All profiles that are followed by ``profile``, i.e. the *followees* of
|
||||||
|
``profile`` (requires :option:`--login`).
|
||||||
|
|
||||||
|
- ``-post``
|
||||||
|
Replace **post** with the post's shortcode to download single post. Must be preceded by ``--`` in
|
||||||
|
the argument list to not be mistaken as an option flag::
|
||||||
|
|
||||||
|
instaloader -- -B_K4CykAOtf
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
.. targets-end
|
||||||
|
|
||||||
|
Instaloader goes through all media matching the specified targets and
|
||||||
|
downloads the pictures and videos and their captions. You can specify
|
||||||
|
|
||||||
|
- :option:`--comments`
|
||||||
|
also **download comments** of each post,
|
||||||
|
|
||||||
|
- :option:`--geotags`
|
||||||
|
**download geotags** of each post and save them as
|
||||||
|
Google Maps link (requires :option:`--login`),
|
||||||
|
|
||||||
|
For a reference of all supported command line options, see
|
||||||
|
:ref:`command-line-options`.
|
||||||
|
|
||||||
|
.. _filename-specification:
|
||||||
|
|
||||||
|
Filename Specification
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
For each target, Instaloader creates a directory named after the target,
|
||||||
|
i.e. ``profile``, ``#hashtag``, ``%location id``, ``:feed``, etc. and therein saves the
|
||||||
|
posts in files named after the post's timestamp.
|
||||||
|
|
||||||
|
:option:`--dirname-pattern` allows to configure the directory name of each
|
||||||
|
target. The default is ``--dirname-pattern={target}``. In the dirname
|
||||||
|
pattern, the token ``{target}`` is replaced by the target name, and
|
||||||
|
``{profile}`` is replaced by the owner of the post which is downloaded.
|
||||||
|
|
||||||
|
:option:`--filename-pattern` configures the path of the post's files relative
|
||||||
|
to the target directory that is specified with :option:`--dirname-pattern`.
|
||||||
|
The default is ``--filename-pattern={date_utc}_UTC``.
|
||||||
|
The tokens ``{target}`` and ``{profile}`` are replaced like in the
|
||||||
|
dirname pattern. The following tokens are defined for usage with
|
||||||
|
:option:`--filename-pattern`:
|
||||||
|
|
||||||
|
- ``{target}``
|
||||||
|
Target name (as given in Instaloader command line)
|
||||||
|
|
||||||
|
- ``{profile}`` (same as ``{owner_username}``)
|
||||||
|
Owner of the Post / StoryItem.
|
||||||
|
|
||||||
|
- ``{owner_id}``
|
||||||
|
Unique integer ID of owner profile.
|
||||||
|
|
||||||
|
- ``{shortcode}``
|
||||||
|
Shortcode (identifier string).
|
||||||
|
|
||||||
|
- ``{mediaid}``
|
||||||
|
Integer representation of shortcode.
|
||||||
|
|
||||||
|
- ``{date_utc}`` (same as ``{date}``)
|
||||||
|
Creation time in UTC timezone.
|
||||||
|
`strftime()-style formatting options <https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior>`__
|
||||||
|
are supported as format specifier. The default date format specifier used by
|
||||||
|
Instaloader is::
|
||||||
|
|
||||||
|
{date_utc:%Y-%m-%d_%H-%M-%S}
|
||||||
|
|
||||||
|
For example, encode the poster's profile name in the filenames with::
|
||||||
|
|
||||||
|
instaloader --filename-pattern={date_utc}_UTC_{profile} "#hashtag"
|
||||||
|
|
||||||
|
As another example, you may instruct Instaloader to store posts in a
|
||||||
|
``PROFILE/YEAR/SHORTCODE.jpg`` directory structure::
|
||||||
|
|
||||||
|
instaloader --dirname-pattern={profile} --filename-pattern={date_utc:%Y}/{shortcode} <target> ...
|
||||||
|
|
||||||
|
.. _filter-posts:
|
||||||
|
|
||||||
|
Filter Posts
|
||||||
|
^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. py:currentmodule:: instaloader
|
||||||
|
|
||||||
|
The options :option:`--post-filter` and :option:`--storyitem-filter`
|
||||||
|
allow to specify criteria that posts or story items have to
|
||||||
|
meet to be downloaded. If not given, all posts are downloaded.
|
||||||
|
|
||||||
|
The filter string must be a
|
||||||
|
`Python boolean expression <https://docs.python.org/3/reference/expressions.html#boolean-operations>`__
|
||||||
|
where the attributes from :class:`Post` or
|
||||||
|
:class:`StoryItem` respectively are defined.
|
||||||
|
|
||||||
|
Id est, the following attributes can be used with both
|
||||||
|
:option:`--post-filter` and :option:`--storyitem-filter`:
|
||||||
|
|
||||||
|
- :attr:`~Post.owner_username` (str), :attr:`~Post.owner_id` (int)
|
||||||
|
Owner profile username / user ID.
|
||||||
|
|
||||||
|
- :attr:`~Post.date_utc` (datetime), :attr:`~Post.date_local` (datetime)
|
||||||
|
Creation timestamp. Since :class:`~datetime.datetime` objects can be created
|
||||||
|
inside filter strings, this easily allows filtering by creation date. E.g.::
|
||||||
|
|
||||||
|
instaloader --post-filter="date_utc <= datetime(2018, 5, 31)" target
|
||||||
|
|
||||||
|
- :attr:`~Post.is_video` (bool)
|
||||||
|
Whether Post/StoryItem is a video. For example, you may skip videos::
|
||||||
|
|
||||||
|
instaloader --post-filter="not is_video" target
|
||||||
|
|
||||||
|
This is not the same as :option:`--no-videos` and
|
||||||
|
:option:`--no-video-thumbnails`, since sidecar posts (posts that contain
|
||||||
|
multiple pictures/videos in one post) have this attribute set to False.
|
||||||
|
|
||||||
|
As :option:`--post-filter`, the following attributes can be used additionally:
|
||||||
|
|
||||||
|
- :attr:`~Post.viewer_has_liked` (bool)
|
||||||
|
Whether user (with :option:`--login`) has liked given post. To download the
|
||||||
|
pictures from your feed that you have liked::
|
||||||
|
|
||||||
|
instaloader --login=your_username --post-filter=viewer_has_liked :feed
|
||||||
|
|
||||||
|
- :attr:`~Post.likes` (int), :attr:`~Post.comments` (int)
|
||||||
|
Likes count / comments count. You might only want to download posts that
|
||||||
|
were either liked by yourself or by many others::
|
||||||
|
|
||||||
|
instaloader --login=your_username --post-filter="likes>100 or viewer_has_liked" profile
|
||||||
|
|
||||||
|
- :attr:`~Post.caption_hashtags` (list of str) / :attr:`~Post.caption_mentions` (list of str)
|
||||||
|
``#hashtags`` or ``@mentions`` (lowercased) in the Post's caption. For example, to
|
||||||
|
download posts of kittens that are cute::
|
||||||
|
|
||||||
|
instaloader --post-filter="'cute' in caption_hashtags" "#kitten"
|
||||||
|
|
||||||
|
- :attr:`~Post.tagged_users` (list of str)
|
||||||
|
Lowercased usernames that are tagged in the Post.
|
||||||
|
|
||||||
|
For :option:`--storyitem-filter`, the following additional attributes are
|
||||||
|
defined:
|
||||||
|
|
||||||
|
- :attr:`~StoryItem.expiring_utc` (datetime) / :attr:`~StoryItem.expiring_local` (datetime)
|
||||||
|
Timestamp when StoryItem will get unavailable.
|
||||||
|
|
||||||
|
.. _metadata-text-files:
|
||||||
|
|
||||||
|
Metadata Text Files
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Unless :option:`--no-captions` is given, Instaloader creates a ``.txt`` file
|
||||||
|
along with each post where the Post's caption is saved.
|
||||||
|
|
||||||
|
You can customize what metadata to save for each Post or StoryItem with
|
||||||
|
:option:`--post-metadata-txt` and :option:`--storyitem-metadata-txt`. The
|
||||||
|
default is ``--post-metadata-txt={caption}`` and no storyitem metadata txt.
|
||||||
|
These strings are formatted similar as the path patterns described in :ref:`filename-specification` and
|
||||||
|
the result is saved in text files, unless it is empty.
|
||||||
|
|
||||||
|
Specifying these options multiple times results in output having multiple lines,
|
||||||
|
in the order they were given to Instaloader.
|
||||||
|
|
||||||
|
The field names are evaluated to :class:`Post` or :class:`StoryItem` attributes,
|
||||||
|
and as such, the same fields are supported as in :ref:`filename-specification`
|
||||||
|
and :ref:`filter-posts`.
|
||||||
|
|
||||||
|
For example, to save the current number of likes for each post, rather than
|
||||||
|
the post's caption::
|
||||||
|
|
||||||
|
instaloader --post-metadata-txt="{likes} likes." <target>
|
||||||
|
|
||||||
|
Note that with this feature, it is possible to easily and fastly extract
|
||||||
|
additional metadata of already-downloaded posts, by reimporting their JSON
|
||||||
|
files. Say, you now also want to export the number of comments the Posts had
|
||||||
|
when they were downloaded::
|
||||||
|
|
||||||
|
instaloader --post-metadata-txt="{likes} likes, {comments} comments." <target>/*.json.xz
|
||||||
|
|
||||||
|
.. _instaloader-as-cronjob:
|
||||||
|
|
||||||
|
Instaloader as Cronjob
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Instaloader is suitable for running as a cronjob to periodically update your
|
||||||
|
personal Instagram archive. The :option:`--quiet` option disables user
|
||||||
|
interactions and logging of non-error messages. To non-interactively use
|
||||||
|
Instaloader logged-in, create a session file::
|
||||||
|
|
||||||
|
instaloader --login=your_username
|
||||||
|
|
||||||
|
Then use the same username in your cronjob to load the session and download
|
||||||
|
the given targets::
|
||||||
|
|
||||||
|
instaloader --login=your_username --quiet target [...]
|
||||||
|
|
||||||
|
Instaloader saves the session file to
|
||||||
|
``~/.config/instaloader/session-YOUR-USERNAME``. See
|
||||||
|
:option:`--sessionfile` option for how to override this path.
|
||||||
|
|
||||||
|
Programming Instaloader
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
If your task cannot be done with the command line interface of Instaloader,
|
||||||
|
consider taking a look at the :ref:`python-module-instaloader`.
|
||||||
|
Instaloader exposes its internally used methods and structures, making it a
|
||||||
|
powerful and intuitive Python API for Instagram, allowing to further customize
|
||||||
|
obtaining media and metadata.
|
||||||
|
|
||||||
|
Also see :ref:`codesnippets`, where we collect a few example scripts that use
|
||||||
|
Instaloader for simple tasks that cannot be done with the command line
|
||||||
|
interface.
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
.. _command-line-options:
|
||||||
|
|
||||||
|
Command Line Options
|
||||||
|
====================
|
||||||
|
|
||||||
|
Instaloader is invoked with::
|
||||||
|
|
||||||
|
$ instaloader [options] target [target ...]
|
||||||
|
|
||||||
|
where ``target`` is a ``profile``, a ``"#hashtag"``, ``@profile`` (all profiles
|
||||||
|
that *profile* is following), ``%location ID``, or if logged in ``:feed`` (pictures from your
|
||||||
|
feed), ``:stories`` (stories of your followees) or ``:saved`` (collection of
|
||||||
|
posts marked as saved).
|
||||||
|
|
||||||
|
Here we explain the additional options that can be given to Instaloader to
|
||||||
|
customize its behavior. For an
|
||||||
|
introduction on how to use Instaloader, see
|
||||||
|
:ref:`download-pictures-from-instagram`.
|
||||||
|
|
||||||
|
To get a list of all flags, their abbreviations and
|
||||||
|
their descriptions, you may also run::
|
||||||
|
|
||||||
|
instaloader --help
|
||||||
|
|
||||||
|
Targets
|
||||||
|
^^^^^^^
|
||||||
|
|
||||||
|
Specify a list of targets. For each of these, Instaloader creates a folder and
|
||||||
|
stores all posts along with the pictures's captions there.
|
||||||
|
|
||||||
|
.. include:: basic-usage.rst
|
||||||
|
:start-after: targets-start
|
||||||
|
:end-before: targets-end
|
||||||
|
|
||||||
|
- ``filename.json[.xz]``
|
||||||
|
Re-Download the given object
|
||||||
|
|
||||||
|
- ``+args.txt``
|
||||||
|
Read targets (and options) from given text file. See :option:`+args.txt`.
|
||||||
|
|
||||||
|
What to Download of each Post
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. option:: --no-pictures
|
||||||
|
|
||||||
|
Do not download post pictures. Cannot be used together with
|
||||||
|
:option:`--fast-update`. Implies :option:`--no-video-thumbnails`, does not
|
||||||
|
imply :option:`--no-videos`.
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
.. option:: --no-videos, -V
|
||||||
|
|
||||||
|
Do not download videos.
|
||||||
|
|
||||||
|
.. option:: --no-video-thumbnails
|
||||||
|
|
||||||
|
Do not download thumbnails of videos.
|
||||||
|
|
||||||
|
.. option:: --geotags, -G
|
||||||
|
|
||||||
|
Download geotags when available. Geotags are stored as a text file with
|
||||||
|
the location's name and a Google Maps link. This requires an additional
|
||||||
|
request to the Instagram server for each picture. Requires :option:`--login`.
|
||||||
|
|
||||||
|
.. option:: --comments, -C
|
||||||
|
|
||||||
|
Download and update comments for each post. This requires an additional
|
||||||
|
request to the Instagram server for each post, which is why it is disabled by
|
||||||
|
default.
|
||||||
|
|
||||||
|
.. option:: --no-captions
|
||||||
|
|
||||||
|
Do not create txt files.
|
||||||
|
|
||||||
|
.. option:: --post-metadata-txt
|
||||||
|
|
||||||
|
Template to write in txt file for each Post. See :ref:`metadata-text-files`.
|
||||||
|
|
||||||
|
.. option:: --storyitem-metadata-txt
|
||||||
|
|
||||||
|
Template to write in txt file for each StoryItem. See
|
||||||
|
:ref:`metadata-text-files`.
|
||||||
|
|
||||||
|
.. option:: --no-metadata-json
|
||||||
|
|
||||||
|
Do not create a JSON file containing the metadata of each post.
|
||||||
|
|
||||||
|
.. option:: --no-compress-json
|
||||||
|
|
||||||
|
Do not xz compress JSON files, rather create pretty formatted JSONs.
|
||||||
|
|
||||||
|
|
||||||
|
What to Download of each Profile
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. option:: --no-posts
|
||||||
|
|
||||||
|
Do not download regular posts.
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
.. option:: --no-profile-pic
|
||||||
|
|
||||||
|
Do not download profile picture.
|
||||||
|
|
||||||
|
.. option:: --stories, -s
|
||||||
|
|
||||||
|
Also download stories of each profile that is downloaded. Requires
|
||||||
|
:option:`--login`.
|
||||||
|
|
||||||
|
.. option:: --highlights
|
||||||
|
|
||||||
|
Also download highlights of each profile that is downloaded. Requires
|
||||||
|
:option:`--login`.
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
.. option:: --tagged
|
||||||
|
|
||||||
|
Also download posts where each profile is tagged.
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
.. option:: --igtv
|
||||||
|
|
||||||
|
Also download IGTV videos.
|
||||||
|
|
||||||
|
.. versionadded:: 4.3
|
||||||
|
|
||||||
|
Which Posts to Download
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. option:: --fast-update, -F
|
||||||
|
|
||||||
|
For each target, stop when encountering the first already-downloaded picture.
|
||||||
|
This flag is recommended when you use Instaloader to update your personal
|
||||||
|
Instagram archive.
|
||||||
|
|
||||||
|
.. option:: --post-filter filter, --only-if filter
|
||||||
|
|
||||||
|
Expression that, if given, must evaluate to True for each post to be
|
||||||
|
downloaded. Must be a syntactically valid Python expression. Variables are
|
||||||
|
evaluated to :class:`instaloader.Post` attributes. Example:
|
||||||
|
``--post-filter=viewer_has_liked``. See :ref:`filter-posts` for more
|
||||||
|
examples.
|
||||||
|
|
||||||
|
.. option:: --storyitem-filter filter
|
||||||
|
|
||||||
|
Expression that, if given, must evaluate to True for each storyitem to be
|
||||||
|
downloaded. Must be a syntactically valid Python expression. Variables are
|
||||||
|
evaluated to :class:`instaloader.StoryItem` attributes.
|
||||||
|
See :ref:`filter-posts` for more examples.
|
||||||
|
|
||||||
|
.. option:: --count COUNT, -c
|
||||||
|
|
||||||
|
Do not attempt to download more than COUNT posts. Applies only to
|
||||||
|
``#hashtag``, ``%location id``, and ``:feed``.
|
||||||
|
|
||||||
|
|
||||||
|
Login (Download Private Profiles)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Instaloader can login to Instagram. This allows downloading private
|
||||||
|
profiles. To login, pass the :option:`--login` option. Your session cookie (not your
|
||||||
|
password!) will be saved to a local file to be reused next time you want
|
||||||
|
Instaloader to login.
|
||||||
|
|
||||||
|
.. option:: --login YOUR-USERNAME, -l YOUR-USERNAME
|
||||||
|
|
||||||
|
Login name (profile name) for your Instagram account.
|
||||||
|
|
||||||
|
.. option:: --sessionfile SESSIONFILE, -f SESSIONFILE
|
||||||
|
|
||||||
|
Path for loading and storing session key file. Defaults to
|
||||||
|
``~/.config/instaloader/session-YOUR-USERNAME``.
|
||||||
|
|
||||||
|
.. option:: --password YOUR-PASSWORD, -p YOUR-PASSWORD
|
||||||
|
|
||||||
|
Password for your Instagram account. Without this option, you'll be prompted
|
||||||
|
for your password interactively if there is not yet a valid session file.
|
||||||
|
|
||||||
|
.. warning:: Using :option:`--password` option is discouraged for security
|
||||||
|
reasons. Enter your password interactively when asked, or use the
|
||||||
|
sessionfile feature (:option:`--sessionfile` to customize path).
|
||||||
|
|
||||||
|
How to Download
|
||||||
|
^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. option:: --dirname-pattern DIRNAME_PATTERN
|
||||||
|
|
||||||
|
Name of directory where to store posts. ``{profile}`` is replaced by the
|
||||||
|
profile name, ``{target}`` is replaced by the target you specified, i.e.
|
||||||
|
either ``:feed``, ``#hashtag`` or the profile name. Defaults to ``{target}``.
|
||||||
|
See :ref:`filename-specification`.
|
||||||
|
|
||||||
|
.. option:: --filename-pattern FILENAME_PATTERN
|
||||||
|
|
||||||
|
Prefix of filenames, relative to the directory given with
|
||||||
|
:option:`--dirname-pattern`. ``{profile}`` is replaced by the profile name,
|
||||||
|
``{target}`` is replaced by the target you specified, i.e. either ``:feed``,
|
||||||
|
``#hashtag`` or the profile name. Defaults to ``{date_utc}_UTC``.
|
||||||
|
See :ref:`filename-specification` for a list of supported tokens.
|
||||||
|
|
||||||
|
.. option:: --resume-prefix prefix
|
||||||
|
|
||||||
|
For many targets, Instaloader is capable of resuming a previously-aborted
|
||||||
|
download loop. To do so, it creates a JSON file within the target directory
|
||||||
|
when interrupted. This option controls the prefix for filenames that are
|
||||||
|
used to save the information to resume an interrupted download. The default
|
||||||
|
prefix is ``iterator``.
|
||||||
|
|
||||||
|
Resuming an interrupted download is supported for most, but not all targets.
|
||||||
|
JSON files with resume information are always compressed, regardless of
|
||||||
|
:option:`--no-compress-json`.
|
||||||
|
|
||||||
|
This feature is turned off entirely with :option:`--no-resume`.
|
||||||
|
|
||||||
|
.. versionadded:: 4.5
|
||||||
|
|
||||||
|
.. option:: --no-resume
|
||||||
|
|
||||||
|
Do not resume a previously-aborted download iteration, and do not save such
|
||||||
|
information when interrupted.
|
||||||
|
|
||||||
|
.. versionadded:: 4.5
|
||||||
|
|
||||||
|
.. option:: --user-agent USER_AGENT
|
||||||
|
|
||||||
|
User Agent to use for HTTP requests. Per default, Instaloader pretends being
|
||||||
|
Chrome/51.
|
||||||
|
|
||||||
|
.. option:: --max-connection-attempts N
|
||||||
|
|
||||||
|
Maximum number of connection attempts until a request is aborted. Defaults
|
||||||
|
to ``3``. If a connection fails, it can be manually skipped by hitting
|
||||||
|
:kbd:`Control-c`. Set this to ``0`` to retry infinitely.
|
||||||
|
|
||||||
|
.. option:: --request-timeout N
|
||||||
|
|
||||||
|
Seconds to wait before timing out a connection request.
|
||||||
|
|
||||||
|
.. versionadded:: 4.3
|
||||||
|
|
||||||
|
Miscellaneous Options
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. option:: --quiet, -q
|
||||||
|
|
||||||
|
Disable user interaction, i.e. do not print messages (except errors) and fail
|
||||||
|
if login credentials are needed but not given.
|
||||||
|
This is handy for running :ref:`instaloader-as-cronjob`.
|
||||||
|
|
||||||
|
.. option:: +args.txt
|
||||||
|
|
||||||
|
Read arguments from file `args.txt`, a shortcut to provide arguments from
|
||||||
|
file rather than command-line. This provides a convenient way to hide login
|
||||||
|
info from CLI, and can also be used to simplify management of long arguments.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Text file should separate arguments with line breaks.
|
||||||
|
|
||||||
|
args.txt example::
|
||||||
|
|
||||||
|
--login=MYUSERNAME
|
||||||
|
--password=MYPASSWORD
|
||||||
|
--fast-update
|
||||||
|
profile1
|
||||||
|
profile2
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
.. _codesnippets:
|
||||||
|
|
||||||
|
Advanced Instaloader Examples
|
||||||
|
=============================
|
||||||
|
|
||||||
|
.. currentmodule:: instaloader
|
||||||
|
|
||||||
|
.. highlight:: python
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:backlinks: none
|
||||||
|
|
||||||
|
Here we present code examples that use the :ref:`python-module-instaloader` for
|
||||||
|
more advanced Instagram downloading or metadata mining than what is possible
|
||||||
|
with the Instaloader command line interface.
|
||||||
|
|
||||||
|
The scripts presented here can be downloaded from our source tree:
|
||||||
|
`instaloader/docs/codesnippets/ <https://github.com/instaloader/instaloader/tree/master/docs/codesnippets>`__
|
||||||
|
|
||||||
|
.. For each code snippet:
|
||||||
|
- title
|
||||||
|
- brief description of what it does / motivation / how it works
|
||||||
|
- code, or link to code
|
||||||
|
- link to discussion issue
|
||||||
|
- link used methods
|
||||||
|
|
||||||
|
Download Posts in a Specific Period
|
||||||
|
-----------------------------------
|
||||||
|
|
||||||
|
To only download Instagram pictures (and metadata) that are within a specific
|
||||||
|
period, you can simply use :func:`~itertools.dropwhile` and
|
||||||
|
:func:`~itertools.takewhile` from :mod:`itertools` on a generator that returns
|
||||||
|
Posts in **exact chronological order**, such as :meth:`Profile.get_posts`.
|
||||||
|
|
||||||
|
.. literalinclude:: codesnippets/121_since_until.py
|
||||||
|
|
||||||
|
See also :class:`Post`, :meth:`Instaloader.download_post`.
|
||||||
|
|
||||||
|
Discussed in :issue:`121`.
|
||||||
|
|
||||||
|
The code example with :func:`~itertools.dropwhile` and
|
||||||
|
:func:`~itertools.takewhile` makes the assumption that the post iterator returns
|
||||||
|
posts in exact chronological order. As discussed in :issue:`666`, the following
|
||||||
|
approach fits for an **almost chronological order**, where up to *k* older posts
|
||||||
|
are inserted into an otherwise chronological order, such as a Hashtag feed.
|
||||||
|
|
||||||
|
.. literalinclude:: codesnippets/666_historical_hashtag_data.py
|
||||||
|
|
||||||
|
Likes of a Profile / Ghost Followers
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
To obtain a list of your inactive followers, i.e. followers that did not like
|
||||||
|
any of your pictures, you can use this approach.
|
||||||
|
|
||||||
|
.. literalinclude:: codesnippets/120_ghost_followers.py
|
||||||
|
|
||||||
|
See also :meth:`Profile.get_posts`, :meth:`Post.get_likes`,
|
||||||
|
:meth:`Profile.get_followers`, :meth:`Instaloader.load_session_from_file`,
|
||||||
|
:meth:`Profile.from_username`.
|
||||||
|
|
||||||
|
Discussed in :issue:`120`.
|
||||||
|
|
||||||
|
Track Deleted Posts
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
This script uses Instaloader to obtain a list of currently-online Instagram and
|
||||||
|
compares it with the set of posts that you already have downloaded. It outputs
|
||||||
|
a list of posts which are online but not offline (i.e. not yet downloaded) and a
|
||||||
|
list of posts which are offline but not online (i.e. deleted in the profile).
|
||||||
|
|
||||||
|
.. literalinclude:: codesnippets/56_track_deleted.py
|
||||||
|
|
||||||
|
See also :func:`load_structure_from_file`, :meth:`Profile.from_username`,
|
||||||
|
:meth:`Profile.get_posts`, :class:`Post`.
|
||||||
|
|
||||||
|
Discussed in :issue:`56`.
|
||||||
|
|
||||||
|
Only one Post per User
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
To download only the single most recent post per user within a hashtag feed,
|
||||||
|
this snippet uses a :class:`set` that contains the users of whom a post has
|
||||||
|
already been downloaded. For each post, it checks whether the post's creator is
|
||||||
|
already contained in that set. If not, the post is downloaded from Instagram and
|
||||||
|
the user is added to that set.
|
||||||
|
|
||||||
|
.. literalinclude:: codesnippets/113_only_one_per_user.py
|
||||||
|
|
||||||
|
See also :class:`Post`, :meth:`Instaloader.download_post`,
|
||||||
|
:attr:`Post.owner_profile`, :class:`Profile`.
|
||||||
|
|
||||||
|
Discussed in :issue:`113`.
|
||||||
|
|
||||||
|
Top X Posts of User
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
With Instaloader, it is easy to download the few most-liked pictures of a user.
|
||||||
|
|
||||||
|
.. literalinclude:: codesnippets/194_top_x_of_user.py
|
||||||
|
|
||||||
|
Discussed in :issue:`194`.
|
||||||
|
|
||||||
|
Metadata JSON Files
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
The JSON files Instaloader saves along with each Post contain all the metadata
|
||||||
|
that has been retrieved from Instagram while downloading the picture and
|
||||||
|
associated required information.
|
||||||
|
|
||||||
|
With `jq <https://stedolan.github.io/jq/>`__, a command-line JSON processor, the
|
||||||
|
metadata can be easily post-processed. For example, Instaloader's JSON files can
|
||||||
|
be pretty-formatted with:
|
||||||
|
|
||||||
|
.. code-block:: none
|
||||||
|
|
||||||
|
xzcat 2018-05-13_11-18-45_UTC.json.xz | jq .node
|
||||||
|
|
||||||
|
However, Instaloader tries to do as few metadata requests as possible, so,
|
||||||
|
depending on how Instaloader has been invoked, it may occur that these files do
|
||||||
|
not contain the complete available metadata structure. Nevertheless, the file
|
||||||
|
can be loaded into Instaloader with :func:`load_structure_from_file` and the
|
||||||
|
required metadata then be accessed via the :class:`Post` or :class:`Profile`
|
||||||
|
attributes, which trigger an Instagram request if that particular information is
|
||||||
|
not present in the JSON file.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import instaloader
|
||||||
|
|
||||||
|
L = instaloader.Instaloader()
|
||||||
|
|
||||||
|
posts = instaloader.Hashtag.from_name(L.context, 'urbanphotography').get_posts()
|
||||||
|
|
||||||
|
users = set()
|
||||||
|
|
||||||
|
for post in posts:
|
||||||
|
if not post.owner_profile in users:
|
||||||
|
L.download_post(post, '#urbanphotography')
|
||||||
|
users.add(post.owner_profile)
|
||||||
|
else:
|
||||||
|
print("{} from {} skipped.".format(post, post.owner_profile))
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import instaloader
|
||||||
|
|
||||||
|
L = instaloader.Instaloader()
|
||||||
|
|
||||||
|
USER = "your_account"
|
||||||
|
PROFILE = USER
|
||||||
|
|
||||||
|
# Load session previously saved with `instaloader -l USERNAME`:
|
||||||
|
L.load_session_from_file(USER)
|
||||||
|
|
||||||
|
profile = instaloader.Profile.from_username(L.context, PROFILE)
|
||||||
|
|
||||||
|
likes = set()
|
||||||
|
print("Fetching likes of all posts of profile {}.".format(profile.username))
|
||||||
|
for post in profile.get_posts():
|
||||||
|
print(post)
|
||||||
|
likes = likes | set(post.get_likes())
|
||||||
|
|
||||||
|
print("Fetching followers of profile {}.".format(profile.username))
|
||||||
|
followers = set(profile.get_followers())
|
||||||
|
|
||||||
|
ghosts = followers - likes
|
||||||
|
|
||||||
|
print("Storing ghosts into file.")
|
||||||
|
with open("inactive-users.txt", 'w') as f:
|
||||||
|
for ghost in ghosts:
|
||||||
|
print(ghost.username, file=f)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from itertools import dropwhile, takewhile
|
||||||
|
|
||||||
|
import instaloader
|
||||||
|
|
||||||
|
L = instaloader.Instaloader()
|
||||||
|
|
||||||
|
posts = instaloader.Profile.from_username(L.context, "instagram").get_posts()
|
||||||
|
|
||||||
|
SINCE = datetime(2015, 5, 1)
|
||||||
|
UNTIL = datetime(2015, 3, 1)
|
||||||
|
|
||||||
|
for post in takewhile(lambda p: p.date > UNTIL, dropwhile(lambda p: p.date > SINCE, posts)):
|
||||||
|
print(post.date)
|
||||||
|
L.download_post(post, "instagram")
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
from itertools import islice
|
||||||
|
from math import ceil
|
||||||
|
|
||||||
|
from instaloader import Instaloader, Profile
|
||||||
|
|
||||||
|
PROFILE = ... # profile to download from
|
||||||
|
X_percentage = 10 # percentage of posts that should be downloaded
|
||||||
|
|
||||||
|
L = Instaloader()
|
||||||
|
|
||||||
|
profile = Profile.from_username(L.context, PROFILE)
|
||||||
|
posts_sorted_by_likes = sorted(profile.get_posts(),
|
||||||
|
key=lambda p: p.likes + p.comments,
|
||||||
|
reverse=True)
|
||||||
|
|
||||||
|
for post in islice(posts_sorted_by_likes, ceil(profile.mediacount * X_percentage / 100)):
|
||||||
|
L.download_post(post, PROFILE)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
from glob import glob
|
||||||
|
from sys import argv
|
||||||
|
from os import chdir
|
||||||
|
|
||||||
|
from instaloader import Instaloader, Post, Profile, load_structure_from_file
|
||||||
|
|
||||||
|
# Instaloader instantiation - you may pass additional arguments to the constructor here
|
||||||
|
L = Instaloader()
|
||||||
|
|
||||||
|
# If desired, load session previously saved with `instaloader -l USERNAME`:
|
||||||
|
#L.load_session_from_file(USERNAME)
|
||||||
|
|
||||||
|
try:
|
||||||
|
TARGET = argv[1]
|
||||||
|
except IndexError:
|
||||||
|
raise SystemExit("Pass profile name as argument!")
|
||||||
|
|
||||||
|
# Obtain set of posts that are on hard disk
|
||||||
|
chdir(TARGET)
|
||||||
|
offline_posts = set(filter(lambda s: isinstance(s, Post),
|
||||||
|
(load_structure_from_file(L.context, file)
|
||||||
|
for file in (glob('*.json.xz') + glob('*.json')))))
|
||||||
|
|
||||||
|
# Obtain set of posts that are currently online
|
||||||
|
post_iterator = Profile.from_username(L.context, TARGET).get_posts()
|
||||||
|
online_posts = set(post_iterator)
|
||||||
|
|
||||||
|
if online_posts - offline_posts:
|
||||||
|
print("Not yet downloaded posts:")
|
||||||
|
print(" ".join(str(p) for p in (online_posts - offline_posts)))
|
||||||
|
|
||||||
|
if offline_posts - online_posts:
|
||||||
|
print("Deleted posts:")
|
||||||
|
print(" ".join(str(p) for p in (offline_posts - online_posts)))
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
from argparse import ArgumentParser
|
||||||
|
from glob import glob
|
||||||
|
from os.path import expanduser
|
||||||
|
from platform import system
|
||||||
|
from sqlite3 import OperationalError, connect
|
||||||
|
|
||||||
|
try:
|
||||||
|
from instaloader import ConnectionException, Instaloader
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
raise SystemExit("Instaloader not found.\n pip install [--user] instaloader")
|
||||||
|
|
||||||
|
|
||||||
|
def get_cookiefile():
|
||||||
|
default_cookiefile = {
|
||||||
|
"Windows": "~/AppData/Roaming/Mozilla/Firefox/Profiles/*/cookies.sqlite",
|
||||||
|
"Darwin": "~/Library/Application Support/Firefox/Profiles/*/cookies.sqlite",
|
||||||
|
}.get(system(), "~/.mozilla/firefox/*/cookies.sqlite")
|
||||||
|
cookiefiles = glob(expanduser(default_cookiefile))
|
||||||
|
if not cookiefiles:
|
||||||
|
raise SystemExit("No Firefox cookies.sqlite file found. Use -c COOKIEFILE.")
|
||||||
|
return cookiefiles[0]
|
||||||
|
|
||||||
|
|
||||||
|
def import_session(cookiefile, sessionfile):
|
||||||
|
print("Using cookies from {}.".format(cookiefile))
|
||||||
|
conn = connect(cookiefile)
|
||||||
|
try:
|
||||||
|
cookie_data = conn.execute(
|
||||||
|
"SELECT name, value FROM moz_cookies WHERE baseDomain='instagram.com'"
|
||||||
|
)
|
||||||
|
except OperationalError:
|
||||||
|
cookie_data = conn.execute(
|
||||||
|
"SELECT name, value FROM moz_cookies WHERE host LIKE '%instagram.com'"
|
||||||
|
)
|
||||||
|
instaloader = Instaloader(max_connection_attempts=1)
|
||||||
|
instaloader.context._session.cookies.update(cookie_data)
|
||||||
|
username = instaloader.test_login()
|
||||||
|
if not username:
|
||||||
|
raise SystemExit("Not logged in. Are you logged in successfully in Firefox?")
|
||||||
|
print("Imported session cookie for {}.".format(username))
|
||||||
|
instaloader.context.username = username
|
||||||
|
instaloader.save_session_to_file(sessionfile)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
p = ArgumentParser()
|
||||||
|
p.add_argument("-c", "--cookiefile")
|
||||||
|
p.add_argument("-f", "--sessionfile")
|
||||||
|
args = p.parse_args()
|
||||||
|
try:
|
||||||
|
import_session(args.cookiefile or get_cookiefile(), args.sessionfile)
|
||||||
|
except (ConnectionException, OperationalError) as e:
|
||||||
|
raise SystemExit("Cookie import failed: {}".format(e))
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import instaloader
|
||||||
|
|
||||||
|
L = instaloader.Instaloader()
|
||||||
|
|
||||||
|
posts = instaloader.Hashtag.from_name(L.context, "urbanphotography").get_posts()
|
||||||
|
|
||||||
|
SINCE = datetime(2020, 5, 10) # further from today, inclusive
|
||||||
|
UNTIL = datetime(2020, 5, 11) # closer to today, not inclusive
|
||||||
|
|
||||||
|
k = 0 # initiate k
|
||||||
|
#k_list = [] # uncomment this to tune k
|
||||||
|
|
||||||
|
for post in posts:
|
||||||
|
postdate = post.date
|
||||||
|
|
||||||
|
if postdate > UNTIL:
|
||||||
|
continue
|
||||||
|
elif postdate <= SINCE:
|
||||||
|
k += 1
|
||||||
|
if k == 50:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
L.download_post(post, "#urbanphotography")
|
||||||
|
# if you want to tune k, uncomment below to get your k max
|
||||||
|
#k_list.append(k)
|
||||||
|
k = 0 # set k to 0
|
||||||
|
|
||||||
|
#max(k_list)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Advanced Instaloader Examples
|
||||||
|
|
||||||
|
This directory contains the scripts that are presented at:
|
||||||
|
https://instaloader.github.io/codesnippets.html
|
||||||
+379
@@ -0,0 +1,379 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# Instaloader documentation build configuration file, created by
|
||||||
|
# sphinx-quickstart on Fri Aug 25 15:38:26 2017.
|
||||||
|
#
|
||||||
|
# This file is execfile()d with the current directory set to its
|
||||||
|
# containing dir.
|
||||||
|
#
|
||||||
|
# Note that not all possible configuration values are present in this
|
||||||
|
# autogenerated file.
|
||||||
|
#
|
||||||
|
# All configuration values have a default; values that are commented out
|
||||||
|
# serve to show the default.
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
#
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import typing
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath('..'))
|
||||||
|
|
||||||
|
import docs.sphinx_autodoc_typehints as sphinx_autodoc_typehints
|
||||||
|
|
||||||
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
#
|
||||||
|
# needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
|
# ones.
|
||||||
|
extensions = [
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinx.ext.githubpages',
|
||||||
|
'sphinx.ext.intersphinx',
|
||||||
|
'sphinx.ext.extlinks'
|
||||||
|
]
|
||||||
|
|
||||||
|
autodoc_default_flags = ['show-inheritance', 'members', 'undoc-members']
|
||||||
|
|
||||||
|
autodoc_member_order = 'bysource'
|
||||||
|
|
||||||
|
intersphinx_mapping = {'python': ('https://docs.python.org/3', None),
|
||||||
|
'requests': ('https://requests.kennethreitz.org/en/master/', None)}
|
||||||
|
|
||||||
|
nitpick_ignore = [('py:class', 'typing.Tuple')]
|
||||||
|
|
||||||
|
current_release = subprocess.check_output(["git", "describe", "--abbrev=0"]).decode("ascii")[1:-1]
|
||||||
|
date_format = "%e %b %Y" if platform.system() != "Windows" else "%d %b %Y"
|
||||||
|
current_release_date = subprocess.check_output(
|
||||||
|
["git", "log", "-1", "--tags", "--format=%ad", "--date=format:"+date_format]).decode("ascii")[:-1]
|
||||||
|
|
||||||
|
html_context = {'current_release': current_release, 'current_release_date': current_release_date}
|
||||||
|
|
||||||
|
extlinks = {
|
||||||
|
'issue': (
|
||||||
|
'https://github.com/instaloader/instaloader/issues/%s',
|
||||||
|
'Issue #'
|
||||||
|
),
|
||||||
|
'example': (
|
||||||
|
'https://raw.githubusercontent.com/instaloader/instaloader/v' + current_release + '/docs/codesnippets/%s',
|
||||||
|
'Example '
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
# The suffix(es) of source filenames.
|
||||||
|
# You can specify multiple suffix as a list of string:
|
||||||
|
#
|
||||||
|
# source_suffix = ['.rst', '.md']
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The encoding of source files.
|
||||||
|
#
|
||||||
|
# source_encoding = 'utf-8-sig'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = 'Instaloader'
|
||||||
|
copyright = '2017-2020, Alexander Graf and Andre Koch-Kramer'
|
||||||
|
author = 'Alexander Graf and Andre Koch-Kramer'
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
# built documents.
|
||||||
|
#
|
||||||
|
# The short X.Y version.
|
||||||
|
#version = '3.0'
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
#release = '3.0rc0'
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#
|
||||||
|
# This is also used if you do content translation via gettext catalogs.
|
||||||
|
# Usually you set "language" from the command line for these cases.
|
||||||
|
language = "en"
|
||||||
|
|
||||||
|
# There are two options for replacing |today|: either, you set today to some
|
||||||
|
# non-false value, then it is used:
|
||||||
|
#
|
||||||
|
# today = ''
|
||||||
|
#
|
||||||
|
# Else, today_fmt is used as the format for a strftime call.
|
||||||
|
#
|
||||||
|
# today_fmt = '%B %d, %Y'
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
# This patterns also effect to html_static_path and html_extra_path
|
||||||
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||||
|
|
||||||
|
# The reST default role (used for this markup: `text`) to use for all
|
||||||
|
# documents.
|
||||||
|
#
|
||||||
|
# default_role = None
|
||||||
|
|
||||||
|
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||||
|
#
|
||||||
|
# add_function_parentheses = True
|
||||||
|
|
||||||
|
# If true, the current module name will be prepended to all description
|
||||||
|
# unit titles (such as .. function::).
|
||||||
|
#
|
||||||
|
add_module_names = False
|
||||||
|
|
||||||
|
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||||
|
# output. They are ignored by default.
|
||||||
|
#
|
||||||
|
# show_authors = False
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'fruity'
|
||||||
|
|
||||||
|
# A list of ignored prefixes for module index sorting.
|
||||||
|
# modindex_common_prefix = []
|
||||||
|
|
||||||
|
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||||
|
# keep_warnings = False
|
||||||
|
|
||||||
|
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||||
|
todo_include_todos = False
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output ----------------------------------------------
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
#
|
||||||
|
html_theme = 'basic'
|
||||||
|
|
||||||
|
# Add any paths that contain custom themes here, relative to this directory.
|
||||||
|
# html_theme_path = []
|
||||||
|
|
||||||
|
# The name for this set of Sphinx documents.
|
||||||
|
# "<project> v<release> documentation" by default.
|
||||||
|
#
|
||||||
|
# html_title = 'Instaloader v3.0rc0'
|
||||||
|
|
||||||
|
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||||
|
#
|
||||||
|
# html_short_title = None
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
|
# of the sidebar.
|
||||||
|
#
|
||||||
|
html_logo = "logo.png"
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to use as a favicon of
|
||||||
|
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
|
# pixels large.
|
||||||
|
#
|
||||||
|
html_favicon = "favicon.ico"
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ['_static']
|
||||||
|
|
||||||
|
# Add any extra paths that contain custom files (such as robots.txt or
|
||||||
|
# .htaccess) here, relative to this directory. These files are copied
|
||||||
|
# directly to the root of the documentation.
|
||||||
|
#
|
||||||
|
# html_extra_path = []
|
||||||
|
|
||||||
|
# If not None, a 'Last updated on:' timestamp is inserted at every page
|
||||||
|
# bottom, using the given strftime format.
|
||||||
|
# The empty string is equivalent to '%b %d, %Y'.
|
||||||
|
#
|
||||||
|
# html_last_updated_fmt = None
|
||||||
|
|
||||||
|
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||||
|
# typographically correct entities.
|
||||||
|
#
|
||||||
|
# html_use_smartypants = True
|
||||||
|
|
||||||
|
# Custom sidebar templates, maps document names to template names.
|
||||||
|
#
|
||||||
|
#html_sidebars = {'**': ["relations.html", "links.html"] }
|
||||||
|
html_sidebars = {}
|
||||||
|
|
||||||
|
# Additional templates that should be rendered to pages, maps page names to
|
||||||
|
# template names.
|
||||||
|
#
|
||||||
|
# html_additional_pages = {}
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#
|
||||||
|
html_domain_indices = False
|
||||||
|
|
||||||
|
# If false, no index is generated.
|
||||||
|
#
|
||||||
|
html_use_index = True
|
||||||
|
|
||||||
|
# If true, the index is split into individual pages for each letter.
|
||||||
|
#
|
||||||
|
# html_split_index = False
|
||||||
|
|
||||||
|
# If true, links to the reST sources are added to the pages.
|
||||||
|
#
|
||||||
|
html_show_sourcelink = False
|
||||||
|
|
||||||
|
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||||
|
#
|
||||||
|
html_show_sphinx = False
|
||||||
|
|
||||||
|
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||||
|
#
|
||||||
|
html_show_copyright = False
|
||||||
|
|
||||||
|
# If true, an OpenSearch description file will be output, and all pages will
|
||||||
|
# contain a <link> tag referring to it. The value of this option must be the
|
||||||
|
# base URL from which the finished HTML is served.
|
||||||
|
#
|
||||||
|
# html_use_opensearch = ''
|
||||||
|
|
||||||
|
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||||
|
# html_file_suffix = None
|
||||||
|
|
||||||
|
# Language to be used for generating the HTML full-text search index.
|
||||||
|
# Sphinx supports the following languages:
|
||||||
|
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
|
||||||
|
# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh'
|
||||||
|
#
|
||||||
|
# html_search_language = 'en'
|
||||||
|
|
||||||
|
# A dictionary with options for the search language support, empty by default.
|
||||||
|
# 'ja' uses this config value.
|
||||||
|
# 'zh' user can custom change `jieba` dictionary path.
|
||||||
|
#
|
||||||
|
# html_search_options = {'type': 'default'}
|
||||||
|
|
||||||
|
# The name of a javascript file (relative to the configuration directory) that
|
||||||
|
# implements a search results scorer. If empty, the default will be used.
|
||||||
|
#
|
||||||
|
# html_search_scorer = 'scorer.js'
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'Instaloaderdoc'
|
||||||
|
|
||||||
|
# -- Options for LaTeX output ---------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#
|
||||||
|
# 'papersize': 'letterpaper',
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
#
|
||||||
|
# 'pointsize': '10pt',
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#
|
||||||
|
# 'preamble': '',
|
||||||
|
|
||||||
|
# Latex figure (float) alignment
|
||||||
|
#
|
||||||
|
# 'figure_align': 'htbp',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title,
|
||||||
|
# author, documentclass [howto, manual, or own class]).
|
||||||
|
latex_documents = [
|
||||||
|
(master_doc, 'Instaloader.tex', 'Instaloader Documentation',
|
||||||
|
'Alexander Graf, André Koch-Kramer', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
|
# the title page.
|
||||||
|
#
|
||||||
|
# latex_logo = None
|
||||||
|
|
||||||
|
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||||
|
# not chapters.
|
||||||
|
#
|
||||||
|
# latex_use_parts = False
|
||||||
|
|
||||||
|
# If true, show page references after internal links.
|
||||||
|
#
|
||||||
|
# latex_show_pagerefs = False
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#
|
||||||
|
# latex_show_urls = False
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#
|
||||||
|
# latex_appendices = []
|
||||||
|
|
||||||
|
# It false, will not define \strong, \code, itleref, \crossref ... but only
|
||||||
|
# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
|
||||||
|
# packages.
|
||||||
|
#
|
||||||
|
# latex_keep_old_macro_names = True
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#
|
||||||
|
# latex_domain_indices = True
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output ---------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
(master_doc, 'instaloader', 'Instaloader Documentation',
|
||||||
|
[author], 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#
|
||||||
|
# man_show_urls = False
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output -------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
(master_doc, 'Instaloader', 'Instaloader Documentation',
|
||||||
|
author, 'Instaloader', 'One line description of project.',
|
||||||
|
'Miscellaneous'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#
|
||||||
|
# texinfo_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#
|
||||||
|
# texinfo_domain_indices = True
|
||||||
|
|
||||||
|
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||||
|
#
|
||||||
|
# texinfo_show_urls = 'footnote'
|
||||||
|
|
||||||
|
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||||
|
#
|
||||||
|
# texinfo_no_detailmenu = False
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
typing.TYPE_CHECKING = True
|
||||||
|
app.add_stylesheet("instaloaderdoc.css")
|
||||||
|
app.connect('autodoc-process-signature', sphinx_autodoc_typehints.process_signature)
|
||||||
|
app.connect('autodoc-process-docstring', sphinx_autodoc_typehints.process_docstring)
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
.. _contributing:
|
||||||
|
|
||||||
|
Contributing to Instaloader
|
||||||
|
===========================
|
||||||
|
|
||||||
|
As an open source project, Instaloader heavily depends on the contributions from
|
||||||
|
its community. In this document, we advise on how you may help Instaloader to
|
||||||
|
become an even greater tool.
|
||||||
|
|
||||||
|
Instaloader's development is organized on
|
||||||
|
`GitHub <https://github.com/instaloader/instaloader>`__, where Issues and Pull
|
||||||
|
Requests are discussed.
|
||||||
|
|
||||||
|
Reporting Bugs
|
||||||
|
--------------
|
||||||
|
|
||||||
|
If you encounter a bug, do not hesitate to report it in our
|
||||||
|
`Issue Tracker <https://github.com/instaloader/instaloader/issues>`__. When
|
||||||
|
reporting a problem, please keep the following in mind:
|
||||||
|
|
||||||
|
- Ensure you **use the latest version** of Instaloader. The currently-installed
|
||||||
|
version can be found out with ``instaloader --version``.
|
||||||
|
|
||||||
|
- Check whether there is a valid solution in our :ref:`troubleshooting` section.
|
||||||
|
|
||||||
|
- Briefly **check whether the bug has already been reported**. If you find an
|
||||||
|
issue reporting the same bug you encountered, comment there rather than
|
||||||
|
opening a new issue. However, if unsure, please create a new issue.
|
||||||
|
|
||||||
|
- State **how the bug can be reproduced**, i.e. how Instaloader was invoked
|
||||||
|
when the problem occurred (of course, you may anonymize profile names etc.).
|
||||||
|
|
||||||
|
- Include all **error messages and tracebacks** in the report.
|
||||||
|
|
||||||
|
- If not obvious, describe **which behavior you expected**
|
||||||
|
instead of what actually happened.
|
||||||
|
|
||||||
|
- If we have closed an issue apparently inadvertently or inappropriately, please
|
||||||
|
let us know.
|
||||||
|
|
||||||
|
Writing Code or Improving the Documentation
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
Changes of the Instaloader source can be proposed as a
|
||||||
|
`Pull Request <https://github.com/instaloader/instaloader/pulls>`__. There are only
|
||||||
|
few things to consider:
|
||||||
|
|
||||||
|
- Base your Pull Request on the ``master`` branch if it fixes a bug,
|
||||||
|
or the ``upcoming/v4.X`` branch (if it exists at the moment of submitting the PR)
|
||||||
|
otherwise.
|
||||||
|
|
||||||
|
- We use `Pylint <https://www.pylint.org/>`__ for error and syntax checking of
|
||||||
|
the source and `MyPy <https://github.com/python/mypy>`__ for type checking.
|
||||||
|
Beware that sometimes it might be better to disable a warning rather than
|
||||||
|
adapting the code to a tool's desires.
|
||||||
|
|
||||||
|
- The documentation source is located in the ``docs`` folder. The file
|
||||||
|
``cli-options.rst`` is merely an RST-formatted copy of ``instaloader --help``
|
||||||
|
output, of which the source is in ``instaloader/__main__.py``.
|
||||||
|
|
||||||
|
Proposing Features
|
||||||
|
------------------
|
||||||
|
|
||||||
|
.. goal-start
|
||||||
|
|
||||||
|
Instaloader's goal is to mimic the browser's behavior to access the data that
|
||||||
|
is available through the Instagram web interface, pack this data into complete
|
||||||
|
and easily-(re)usable python objects, and provide a user interface for the most
|
||||||
|
common downloading and metadata collection tasks, without changing any of the
|
||||||
|
online data.
|
||||||
|
|
||||||
|
.. goal-end
|
||||||
|
|
||||||
|
Prior spending effort on implementing a new feature, it might be appropriate to
|
||||||
|
clarify how it could fit into the project's scope or discuss implementation
|
||||||
|
details. If you feel the need to do so, please create a "feature suggestion".
|
||||||
|
|
||||||
|
- Instaloader already has plenty of features. **Check the documentation**
|
||||||
|
beforehand to ensure your desired suggestion is not already implemented.
|
||||||
|
|
||||||
|
- Briefly **ensure that your idea has not already been suggested**. If you find
|
||||||
|
an issue suggesting the same or a similar feature, share your thoughts in a
|
||||||
|
comment there, instead of opening a new issue.
|
||||||
|
|
||||||
|
- **Motivate the feature**, i.e.
|
||||||
|
|
||||||
|
- Provide us a **use case of the feature**: How could the user
|
||||||
|
invoke the new function? Which problem would it solve? If new information is
|
||||||
|
obtained, how would it be further processed?
|
||||||
|
|
||||||
|
- Describe already-working **alternatives of the feature** and how they
|
||||||
|
compare to your proposed feature.
|
||||||
|
|
||||||
|
- Briefly describe how your suggested feature **conforms with Instaloader's
|
||||||
|
project goal**.
|
||||||
|
|
||||||
|
- Explain your **solution ideas**. Describe your ideas on how the feature could
|
||||||
|
be implemented and the underlying problem could be solved. Also **describe
|
||||||
|
alternatives** that you have considered.
|
||||||
|
|
||||||
|
Donations
|
||||||
|
---------
|
||||||
|
|
||||||
|
.. donations-start
|
||||||
|
|
||||||
|
It is a pleasure for us to share our Instaloader to the world, and we are proud
|
||||||
|
to have attracted such an active and motivating community, with so many users
|
||||||
|
who share their suggestions and ideas with us. Buying a community-sponsored beer
|
||||||
|
or coffee from time to time is very likely to further raise our passion for the
|
||||||
|
development of Instaloader.
|
||||||
|
|
||||||
|
| For donations, we provide a PayPal.Me link and a Bitcoin address.
|
||||||
|
| `PayPal.me/aandergr <https://www.paypal.me/aandergr>`__
|
||||||
|
| BTC: 1Nst4LoadeYzrKjJ1DX9CpbLXBYE9RKLwY
|
||||||
|
|
||||||
|
.. donations-end
|
||||||
|
|
||||||
|
.. (Discussion in :issue:`130`)
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 330 KiB |
+103
@@ -0,0 +1,103 @@
|
|||||||
|
.. meta::
|
||||||
|
:description:
|
||||||
|
Free command line tool to download photos from Instagram.
|
||||||
|
Scrapes public and private profiles, hashtags, stories, feeds,
|
||||||
|
saved media, and their metadata, comments and captions.
|
||||||
|
Written in Python.
|
||||||
|
|
||||||
|
.. title:: Instaloader — Download Instagram Photos and Metadata
|
||||||
|
|
||||||
|
Instaloader
|
||||||
|
===========
|
||||||
|
|
||||||
|
.. highlight:: none
|
||||||
|
|
||||||
|
**Instaloader** is a tool to download pictures (or videos) along with
|
||||||
|
their captions and other metadata from Instagram.
|
||||||
|
|
||||||
|
.. include:: ../README.rst
|
||||||
|
:start-after: badges-start
|
||||||
|
:end-before: badges-end
|
||||||
|
|
||||||
|
With `Python <https://www.python.org/>`__ installed, do::
|
||||||
|
|
||||||
|
$ pip3 install instaloader
|
||||||
|
|
||||||
|
$ instaloader profile [profile ...]
|
||||||
|
|
||||||
|
See :ref:`install` for more options on how to install Instaloader.
|
||||||
|
|
||||||
|
**Instaloader**
|
||||||
|
|
||||||
|
- downloads **public and private profiles, hashtags, user stories,
|
||||||
|
feeds and saved media**,
|
||||||
|
|
||||||
|
- downloads **comments, geotags and captions** of each post,
|
||||||
|
|
||||||
|
- automatically **detects profile name changes** and renames the target
|
||||||
|
directory accordingly,
|
||||||
|
|
||||||
|
- allows **fine-grained customization** of filters and where to store
|
||||||
|
downloaded media,
|
||||||
|
|
||||||
|
- automatically **resumes previously-interrupted** download iterations,
|
||||||
|
|
||||||
|
- is free `open source <https://github.com/instaloader/instaloader>`__
|
||||||
|
software written in Python.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
instaloader [--comments] [--geotags]
|
||||||
|
[--stories] [--highlights] [--tagged] [--igtv]
|
||||||
|
[--login YOUR-USERNAME] [--fast-update]
|
||||||
|
profile | "#hashtag" | %location_id |
|
||||||
|
:stories | :feed | :saved
|
||||||
|
|
||||||
|
See :ref:`download-pictures-from-instagram` for a detailed introduction on how
|
||||||
|
to use Instaloader to download pictures from Instagram.
|
||||||
|
|
||||||
|
Instaloader Documentation
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
installation
|
||||||
|
basic-usage
|
||||||
|
cli-options
|
||||||
|
as-module
|
||||||
|
codesnippets
|
||||||
|
troubleshooting
|
||||||
|
contributing
|
||||||
|
|
||||||
|
Useful Links
|
||||||
|
------------
|
||||||
|
|
||||||
|
- `Git Repository (on GitHub) <https://github.com/instaloader/instaloader>`__
|
||||||
|
- `PyPI Project Page <https://pypi.org/project/instaloader/>`__
|
||||||
|
- `Issue Tracker / Bug Tracker <https://github.com/instaloader/instaloader/issues>`__
|
||||||
|
- `Version History <https://github.com/instaloader/instaloader/releases>`__
|
||||||
|
|
||||||
|
Disclaimer
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. include:: ../README.rst
|
||||||
|
:start-after: disclaimer-start
|
||||||
|
:end-before: disclaimer-end
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
------------
|
||||||
|
|
||||||
|
As an open source project, Instaloader heavily depends on the contributions from
|
||||||
|
its community. See :ref:`contributing` for how you may help Instaloader to
|
||||||
|
become an even greater tool.
|
||||||
|
|
||||||
|
.. include:: contributing.rst
|
||||||
|
:start-after: donations-start
|
||||||
|
:end-before: donations-end
|
||||||
|
|
||||||
|
..
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
.. _install:
|
||||||
|
|
||||||
|
Install Instaloader
|
||||||
|
===================
|
||||||
|
|
||||||
|
.. highlight:: none
|
||||||
|
|
||||||
|
To **install Instaloader**,
|
||||||
|
|
||||||
|
#. Ensure that you have `Python <https://www.python.org/>`__, at least
|
||||||
|
version 3.5, and `pip <https://pypi.python.org/pypi/pip>`__
|
||||||
|
installed.
|
||||||
|
|
||||||
|
#. Then, install Instaloader using::
|
||||||
|
|
||||||
|
pip3 install instaloader
|
||||||
|
|
||||||
|
To **upgrade Instaloader** to its current version, do::
|
||||||
|
|
||||||
|
pip3 install --upgrade instaloader
|
||||||
|
|
||||||
|
|
||||||
|
**Alternative methods** for installing Instaloader:
|
||||||
|
|
||||||
|
- If you do not want to use pip, even though it is highly recommended,
|
||||||
|
and prefer to **install Instaloader manually**,
|
||||||
|
`Download the Source <https://github.com/instaloader/instaloader/releases/latest>`__,
|
||||||
|
extract the Zip or Tarball and run ``setup.py`` from there.
|
||||||
|
|
||||||
|
- On **Arch Linux**, you may install Instaloader using the
|
||||||
|
`Instaloader AUR package <https://aur.archlinux.org/packages/instaloader/>`__.
|
||||||
|
|
||||||
|
- On **Windows 10**, you may download the standalone executable from the
|
||||||
|
`current release page <https://github.com/instaloader/instaloader/releases/latest>`__.
|
||||||
|
|
||||||
|
- To test the most current **pre-release** version of Instaloader::
|
||||||
|
|
||||||
|
pip3 install --pre instaloader
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
+117
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
@@ -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
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
``Instaloader`` (Main Class)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. module:: instaloader
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
.. highlight:: python
|
||||||
|
|
||||||
|
.. autoclass:: Instaloader
|
||||||
|
:no-show-inheritance:
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
requests
|
||||||
|
sphinx~=2.3
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
# sphinx_autodoc_typehints from Nov 20 2018 (version 1.5.2).
|
||||||
|
# https://github.com/agronholm/sphinx-autodoc-typehints
|
||||||
|
# (slightly modified to fix class links, maybe related to agronholm/sphinx-autodoc-typehints#38)
|
||||||
|
#
|
||||||
|
# This is the MIT license: http://www.opensource.org/licenses/mit-license.php
|
||||||
|
#
|
||||||
|
# Copyright (c) Alex Grönholm
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||||
|
# software and associated documentation files (the "Software"), to deal in the Software
|
||||||
|
# without restriction, including without limitation the rights to use, copy, modify, merge,
|
||||||
|
# publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
|
||||||
|
# to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all copies or
|
||||||
|
# substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||||
|
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||||
|
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
|
||||||
|
# FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
||||||
|
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
# DEALINGS IN THE SOFTWARE.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import inspect
|
||||||
|
from typing import get_type_hints, TypeVar, Any, AnyStr, Generic, Union
|
||||||
|
|
||||||
|
from sphinx.util import logging
|
||||||
|
from sphinx.util.inspect import Signature
|
||||||
|
|
||||||
|
try:
|
||||||
|
from inspect import unwrap
|
||||||
|
except ImportError:
|
||||||
|
def unwrap(func, *, stop=None):
|
||||||
|
"""This is the inspect.unwrap() method copied from Python 3.5's standard library."""
|
||||||
|
if stop is None:
|
||||||
|
def _is_wrapper(f):
|
||||||
|
return hasattr(f, '__wrapped__')
|
||||||
|
else:
|
||||||
|
def _is_wrapper(f):
|
||||||
|
return hasattr(f, '__wrapped__') and not stop(f)
|
||||||
|
f = func # remember the original func for error reporting
|
||||||
|
memo = {id(f)} # Memoise by id to tolerate non-hashable objects
|
||||||
|
while _is_wrapper(func):
|
||||||
|
func = func.__wrapped__
|
||||||
|
id_func = id(func)
|
||||||
|
if id_func in memo:
|
||||||
|
raise ValueError('wrapper loop when unwrapping {!r}'.format(f))
|
||||||
|
memo.add(id_func)
|
||||||
|
return func
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def format_annotation(annotation):
|
||||||
|
if inspect.isclass(annotation) and annotation.__module__ == 'builtins':
|
||||||
|
if annotation.__qualname__ == 'NoneType':
|
||||||
|
return '``None``'
|
||||||
|
else:
|
||||||
|
return ':py:class:`{}`'.format(annotation.__qualname__)
|
||||||
|
|
||||||
|
annotation_cls = annotation if inspect.isclass(annotation) else type(annotation)
|
||||||
|
class_name = None
|
||||||
|
if annotation_cls.__module__ == 'typing':
|
||||||
|
params = None
|
||||||
|
prefix = ':py:class:'
|
||||||
|
module = 'typing'
|
||||||
|
extra = ''
|
||||||
|
|
||||||
|
if inspect.isclass(getattr(annotation, '__origin__', None)):
|
||||||
|
annotation_cls = annotation.__origin__
|
||||||
|
try:
|
||||||
|
if Generic in annotation_cls.mro():
|
||||||
|
module = annotation_cls.__module__
|
||||||
|
except TypeError:
|
||||||
|
pass # annotation_cls was either the "type" object or typing.Type
|
||||||
|
|
||||||
|
if annotation is Any:
|
||||||
|
return ':py:data:`~typing.Any`'
|
||||||
|
elif annotation is AnyStr:
|
||||||
|
return ':py:data:`~typing.AnyStr`'
|
||||||
|
elif isinstance(annotation, TypeVar):
|
||||||
|
return '\\%r' % annotation
|
||||||
|
elif (annotation is Union or getattr(annotation, '__origin__', None) is Union or
|
||||||
|
hasattr(annotation, '__union_params__')):
|
||||||
|
prefix = ':py:data:'
|
||||||
|
class_name = 'Union'
|
||||||
|
if hasattr(annotation, '__union_params__'):
|
||||||
|
params = annotation.__union_params__
|
||||||
|
elif hasattr(annotation, '__args__'):
|
||||||
|
params = annotation.__args__
|
||||||
|
|
||||||
|
if params and len(params) == 2 and (hasattr(params[1], '__qualname__') and
|
||||||
|
params[1].__qualname__ == 'NoneType'):
|
||||||
|
class_name = 'Optional'
|
||||||
|
params = (params[0],)
|
||||||
|
elif annotation_cls.__qualname__ == 'Tuple' and hasattr(annotation, '__tuple_params__'):
|
||||||
|
params = annotation.__tuple_params__
|
||||||
|
if annotation.__tuple_use_ellipsis__:
|
||||||
|
params += (Ellipsis,)
|
||||||
|
elif annotation_cls.__qualname__ == 'Callable':
|
||||||
|
prefix = ':py:data:'
|
||||||
|
arg_annotations = result_annotation = None
|
||||||
|
if hasattr(annotation, '__result__'):
|
||||||
|
arg_annotations = annotation.__args__
|
||||||
|
result_annotation = annotation.__result__
|
||||||
|
elif getattr(annotation, '__args__', None):
|
||||||
|
arg_annotations = annotation.__args__[:-1]
|
||||||
|
result_annotation = annotation.__args__[-1]
|
||||||
|
|
||||||
|
if arg_annotations in (Ellipsis, (Ellipsis,)):
|
||||||
|
params = [Ellipsis, result_annotation]
|
||||||
|
elif arg_annotations is not None:
|
||||||
|
params = [
|
||||||
|
'\\[{}]'.format(
|
||||||
|
', '.join(format_annotation(param) for param in arg_annotations)),
|
||||||
|
result_annotation
|
||||||
|
]
|
||||||
|
elif hasattr(annotation, 'type_var'):
|
||||||
|
# Type alias
|
||||||
|
class_name = annotation.name
|
||||||
|
params = (annotation.type_var,)
|
||||||
|
elif getattr(annotation, '__args__', None) is not None:
|
||||||
|
params = annotation.__args__
|
||||||
|
elif hasattr(annotation, '__parameters__'):
|
||||||
|
params = annotation.__parameters__
|
||||||
|
|
||||||
|
if params:
|
||||||
|
extra = '\\[{}]'.format(', '.join(format_annotation(param) for param in params))
|
||||||
|
|
||||||
|
if not class_name:
|
||||||
|
class_name = annotation_cls.__qualname__.title()
|
||||||
|
|
||||||
|
return '{}`~{}.{}`{}'.format(prefix, module, class_name, extra)
|
||||||
|
elif annotation is Ellipsis:
|
||||||
|
return '...'
|
||||||
|
elif inspect.isclass(annotation) or inspect.isclass(getattr(annotation, '__origin__', None)):
|
||||||
|
if not inspect.isclass(annotation):
|
||||||
|
annotation_cls = annotation.__origin__
|
||||||
|
|
||||||
|
extra = ''
|
||||||
|
if Generic in annotation_cls.mro():
|
||||||
|
params = (getattr(annotation, '__parameters__', None) or
|
||||||
|
getattr(annotation, '__args__', None))
|
||||||
|
extra = '\\[{}]'.format(', '.join(format_annotation(param) for param in params))
|
||||||
|
|
||||||
|
module = annotation.__module__.split('.')[0] # hack to 'fix' class linking for Instaloader project
|
||||||
|
return ':py:class:`~{}.{}`{}'.format(module, annotation_cls.__qualname__,
|
||||||
|
extra)
|
||||||
|
|
||||||
|
return str(annotation)
|
||||||
|
|
||||||
|
|
||||||
|
def process_signature(app, what: str, name: str, obj, options, signature, return_annotation):
|
||||||
|
if not callable(obj):
|
||||||
|
return
|
||||||
|
|
||||||
|
if what in ('class', 'exception'):
|
||||||
|
obj = getattr(obj, '__init__', getattr(obj, '__new__', None))
|
||||||
|
|
||||||
|
if not getattr(obj, '__annotations__', None):
|
||||||
|
return
|
||||||
|
|
||||||
|
obj = unwrap(obj)
|
||||||
|
signature = Signature(obj)
|
||||||
|
parameters = [
|
||||||
|
param.replace(annotation=inspect.Parameter.empty)
|
||||||
|
for param in signature.signature.parameters.values()
|
||||||
|
]
|
||||||
|
|
||||||
|
if parameters:
|
||||||
|
if what in ('class', 'exception'):
|
||||||
|
del parameters[0]
|
||||||
|
elif what == 'method':
|
||||||
|
outer = inspect.getmodule(obj)
|
||||||
|
for clsname in obj.__qualname__.split('.')[:-1]:
|
||||||
|
outer = getattr(outer, clsname)
|
||||||
|
|
||||||
|
method_name = obj.__name__
|
||||||
|
if method_name.startswith("__") and not method_name.endswith("__"):
|
||||||
|
# If the method starts with double underscore (dunder)
|
||||||
|
# Python applies mangling so we need to prepend the class name.
|
||||||
|
# This doesn't happen if it always ends with double underscore.
|
||||||
|
class_name = obj.__qualname__.split('.')[-2]
|
||||||
|
method_name = "_{c}{m}".format(c=class_name, m=method_name)
|
||||||
|
|
||||||
|
method_object = outer.__dict__[method_name]
|
||||||
|
if not isinstance(method_object, (classmethod, staticmethod)):
|
||||||
|
del parameters[0]
|
||||||
|
|
||||||
|
signature.signature = signature.signature.replace(
|
||||||
|
parameters=parameters,
|
||||||
|
return_annotation=inspect.Signature.empty)
|
||||||
|
|
||||||
|
return signature.format_args().replace('\\', '\\\\'), None
|
||||||
|
|
||||||
|
|
||||||
|
def process_docstring(app, what, name, obj, options, lines):
|
||||||
|
if isinstance(obj, property):
|
||||||
|
obj = obj.fget
|
||||||
|
|
||||||
|
if callable(obj):
|
||||||
|
if what in ('class', 'exception'):
|
||||||
|
obj = getattr(obj, '__init__')
|
||||||
|
|
||||||
|
obj = unwrap(obj)
|
||||||
|
try:
|
||||||
|
type_hints = get_type_hints(obj)
|
||||||
|
except (AttributeError, TypeError):
|
||||||
|
# Introspecting a slot wrapper will raise TypeError
|
||||||
|
return
|
||||||
|
except NameError as exc:
|
||||||
|
logger.warning('Cannot resolve forward reference in type annotations of "%s": %s',
|
||||||
|
name, exc)
|
||||||
|
type_hints = obj.__annotations__
|
||||||
|
|
||||||
|
for argname, annotation in type_hints.items():
|
||||||
|
if argname.endswith('_'):
|
||||||
|
argname = '{}\\_'.format(argname[:-1])
|
||||||
|
|
||||||
|
formatted_annotation = format_annotation(annotation)
|
||||||
|
|
||||||
|
if argname == 'return':
|
||||||
|
if what in ('class', 'exception'):
|
||||||
|
# Don't add return type None from __init__()
|
||||||
|
continue
|
||||||
|
|
||||||
|
insert_index = len(lines)
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.startswith(':rtype:'):
|
||||||
|
insert_index = None
|
||||||
|
break
|
||||||
|
elif line.startswith(':return:') or line.startswith(':returns:'):
|
||||||
|
insert_index = i
|
||||||
|
|
||||||
|
if insert_index is not None:
|
||||||
|
if insert_index == len(lines):
|
||||||
|
# Ensure that :rtype: doesn't get joined with a paragraph of text, which
|
||||||
|
# prevents it being interpreted.
|
||||||
|
lines.append('')
|
||||||
|
insert_index += 1
|
||||||
|
|
||||||
|
lines.insert(insert_index, ':rtype: {}'.format(formatted_annotation))
|
||||||
|
else:
|
||||||
|
searchfor = ':param {}:'.format(argname)
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.startswith(searchfor):
|
||||||
|
lines.insert(i, ':type {}: {}'.format(argname, formatted_annotation))
|
||||||
|
break
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
.. _troubleshooting:
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
===============
|
||||||
|
|
||||||
|
.. highlight:: python
|
||||||
|
|
||||||
|
429 Too Many Requests
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
Instaloader has a logic to keep track of its requests to Instagram and to obey
|
||||||
|
their rate limits. Since they are nowhere documented, we try them out
|
||||||
|
experimentally. We have a daily cron job running to confirm that Instaloader
|
||||||
|
still stays within the rate limits. Nevertheless, the rate control logic assumes
|
||||||
|
that
|
||||||
|
|
||||||
|
- at one time, Instaloader is the only application that consumes requests, i.e.
|
||||||
|
neither the Instagram browser interface, nor a mobile app, nor another
|
||||||
|
Instaloader instance is running in parallel, and
|
||||||
|
|
||||||
|
- no requests had been consumed when Instaloader starts.
|
||||||
|
|
||||||
|
The latter one implies that restarting or reinstantiating Instaloader often
|
||||||
|
within short time is prone to cause a 429. If a request is denied with a 429,
|
||||||
|
Instaloader retries the request as soon as the temporary ban is assumed to be
|
||||||
|
expired. In case the retry continuously fails for some reason, which should not
|
||||||
|
happen under normal conditions, consider adjusting the
|
||||||
|
:option:`--max-connection-attempts` option.
|
||||||
|
|
||||||
|
There have been observations that services, that in their nature offer
|
||||||
|
promiscuous IP addresses, such as cloud, VPN and public proxy services, might be
|
||||||
|
subject to significantly stricter limits for anonymous access. However,
|
||||||
|
logged-in accesses (see :option:`--login`) do not seem to be affected.
|
||||||
|
|
||||||
|
Too many queries in the last time
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
**"Too many queries in the last time"** is not an error. It is a notice that the
|
||||||
|
rate limit has almost been reached, according to Instaloader's own rate
|
||||||
|
accounting mechanism. We regularly adjust this mechanism to match Instagram's
|
||||||
|
current rate limiting.
|
||||||
|
|
||||||
|
Private but not followed
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
You have to follow a private account to access most of its associated
|
||||||
|
information.
|
||||||
|
|
||||||
|
Login error
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Instaloader's login *should* work fine, both with and without
|
||||||
|
Two-Factor-Authentication. It also supports handling the *checkpoint challenge*,
|
||||||
|
issued when Instagram suspects authentication activity on your account, by
|
||||||
|
pointing the user to an URL to be opened in a browser.
|
||||||
|
|
||||||
|
Nevertheless, in :issue:`92` and :issue:`615` users reported problems with
|
||||||
|
logging in. We recommend to always keep the session file which Instaloader
|
||||||
|
creates when using :option:`--login`. If a session file is present,
|
||||||
|
:option:`--login` does not make make use of the failure-prone login procedure.
|
||||||
|
Also, session files usually do not expire and can be copied between different
|
||||||
|
computers without any problems.
|
||||||
|
|
||||||
|
If you do not have a session file present, you may use the following script
|
||||||
|
(:example:`615_import_firefox_session.py`) to workaround login problems by
|
||||||
|
importing the session cookies from Firefox and bypassing Instaloader's login and
|
||||||
|
so still use Instaloader's logged-in functionality.
|
||||||
|
|
||||||
|
.. literalinclude:: codesnippets/615_import_firefox_session.py
|
||||||
|
|
||||||
|
To use this script,
|
||||||
|
|
||||||
|
#. Download the script: :example:`615_import_firefox_session.py`,
|
||||||
|
|
||||||
|
#. Login to Instagram in Firefox,
|
||||||
|
|
||||||
|
#. Execute the snippet, e.g. with ``python 615_import_firefox_session.py``,
|
||||||
|
|
||||||
|
#. Then, ``instaloader -l USERNAME`` should work fine.
|
||||||
|
|
||||||
|
This script also supports specifying a cookie file path, which may be useful if
|
||||||
|
you use multiple Firefox profiles or if your operating system has the directory
|
||||||
|
structure differently set up. Also, you can specify an alternative session file
|
||||||
|
path.
|
||||||
+2
-1203
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
|||||||
|
"""Download pictures (or videos) along with their captions and other metadata from Instagram."""
|
||||||
|
|
||||||
|
|
||||||
|
__version__ = '4.5'
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# pylint:disable=wrong-import-position
|
||||||
|
import win_unicode_console # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
win_unicode_console.enable()
|
||||||
|
|
||||||
|
from .exceptions import *
|
||||||
|
from .instaloader import Instaloader
|
||||||
|
from .instaloadercontext import InstaloaderContext, RateController
|
||||||
|
from .nodeiterator import NodeIterator, FrozenNodeIterator, resumable_iteration
|
||||||
|
from .structures import (Hashtag, Highlight, Post, PostSidecarNode, PostComment, PostCommentAnswer, PostLocation,
|
||||||
|
Profile, Story, StoryItem, TopSearchResults, load_structure_from_file, save_structure_to_file)
|
||||||
@@ -0,0 +1,450 @@
|
|||||||
|
"""Download pictures (or videos) along with their captions and other metadata from Instagram."""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from argparse import ArgumentParser, SUPPRESS
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from . import (Instaloader, InstaloaderException, InvalidArgumentException, Post, Profile, ProfileNotExistsException,
|
||||||
|
StoryItem, __version__, load_structure_from_file, TwoFactorAuthRequiredException,
|
||||||
|
BadCredentialsException)
|
||||||
|
from .instaloader import get_default_session_filename
|
||||||
|
from .instaloadercontext import default_user_agent
|
||||||
|
|
||||||
|
|
||||||
|
def usage_string():
|
||||||
|
# NOTE: duplicated in README.rst and docs/index.rst
|
||||||
|
argv0 = os.path.basename(sys.argv[0])
|
||||||
|
argv0 = "instaloader" if argv0 == "__main__.py" else argv0
|
||||||
|
return """
|
||||||
|
{0} [--comments] [--geotags]
|
||||||
|
{2:{1}} [--stories] [--highlights] [--tagged] [--igtv]
|
||||||
|
{2:{1}} [--login YOUR-USERNAME] [--fast-update]
|
||||||
|
{2:{1}} profile | "#hashtag" | %%location_id | :stories | :feed | :saved
|
||||||
|
{0} --help""".format(argv0, len(argv0), '')
|
||||||
|
|
||||||
|
|
||||||
|
def filterstr_to_filterfunc(filter_str: str, item_type: type):
|
||||||
|
"""Takes an --post-filter=... or --storyitem-filter=... filter
|
||||||
|
specification and makes a filter_func Callable out of it."""
|
||||||
|
|
||||||
|
# The filter_str is parsed, then all names occurring in its AST are replaced by loads to post.<name>. A
|
||||||
|
# function Post->bool is returned which evaluates the filter with the post as 'post' in its namespace.
|
||||||
|
|
||||||
|
class TransformFilterAst(ast.NodeTransformer):
|
||||||
|
def visit_Name(self, node: ast.Name):
|
||||||
|
# pylint:disable=no-self-use
|
||||||
|
if not isinstance(node.ctx, ast.Load):
|
||||||
|
raise InvalidArgumentException("Invalid filter: Modifying variables ({}) not allowed.".format(node.id))
|
||||||
|
if node.id == "datetime":
|
||||||
|
return node
|
||||||
|
if not hasattr(item_type, node.id):
|
||||||
|
raise InvalidArgumentException("Invalid filter: {} not a {} attribute.".format(node.id,
|
||||||
|
item_type.__name__))
|
||||||
|
new_node = ast.Attribute(ast.copy_location(ast.Name('item', ast.Load()), node), node.id,
|
||||||
|
ast.copy_location(ast.Load(), node))
|
||||||
|
return ast.copy_location(new_node, node)
|
||||||
|
|
||||||
|
input_filename = '<command line filter parameter>'
|
||||||
|
compiled_filter = compile(TransformFilterAst().visit(ast.parse(filter_str, filename=input_filename, mode='eval')),
|
||||||
|
filename=input_filename, mode='eval')
|
||||||
|
|
||||||
|
def filterfunc(item) -> bool:
|
||||||
|
# pylint:disable=eval-used
|
||||||
|
return bool(eval(compiled_filter, {'item': item, 'datetime': datetime.datetime}))
|
||||||
|
|
||||||
|
return filterfunc
|
||||||
|
|
||||||
|
|
||||||
|
def _main(instaloader: Instaloader, targetlist: List[str],
|
||||||
|
username: Optional[str] = None, password: Optional[str] = None,
|
||||||
|
sessionfile: Optional[str] = None,
|
||||||
|
download_profile_pic: bool = True, download_posts=True,
|
||||||
|
download_stories: bool = False,
|
||||||
|
download_highlights: bool = False,
|
||||||
|
download_tagged: bool = False,
|
||||||
|
download_igtv: bool = False,
|
||||||
|
fast_update: bool = False,
|
||||||
|
max_count: Optional[int] = None, post_filter_str: Optional[str] = None,
|
||||||
|
storyitem_filter_str: Optional[str] = None) -> None:
|
||||||
|
"""Download set of profiles, hashtags etc. and handle logging in and session files if desired."""
|
||||||
|
# Parse and generate filter function
|
||||||
|
post_filter = None
|
||||||
|
if post_filter_str is not None:
|
||||||
|
post_filter = filterstr_to_filterfunc(post_filter_str, Post)
|
||||||
|
instaloader.context.log('Only download posts with property "{}".'.format(post_filter_str))
|
||||||
|
storyitem_filter = None
|
||||||
|
if storyitem_filter_str is not None:
|
||||||
|
storyitem_filter = filterstr_to_filterfunc(storyitem_filter_str, StoryItem)
|
||||||
|
instaloader.context.log('Only download storyitems with property "{}".'.format(storyitem_filter_str))
|
||||||
|
# Login, if desired
|
||||||
|
if username is not None:
|
||||||
|
if not re.match(r"^[A-Za-z0-9._]+$", username):
|
||||||
|
instaloader.context.error("Warning: Parameter \"{}\" for --login is not a valid username.".format(username))
|
||||||
|
try:
|
||||||
|
instaloader.load_session_from_file(username, sessionfile)
|
||||||
|
except FileNotFoundError as err:
|
||||||
|
if sessionfile is not None:
|
||||||
|
print(err, file=sys.stderr)
|
||||||
|
instaloader.context.log("Session file does not exist yet - Logging in.")
|
||||||
|
if not instaloader.context.is_logged_in or username != instaloader.test_login():
|
||||||
|
if password is not None:
|
||||||
|
try:
|
||||||
|
instaloader.login(username, password)
|
||||||
|
except TwoFactorAuthRequiredException:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
code = input("Enter 2FA verification code: ")
|
||||||
|
instaloader.two_factor_login(code)
|
||||||
|
break
|
||||||
|
except BadCredentialsException:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
instaloader.interactive_login(username)
|
||||||
|
instaloader.context.log("Logged in as %s." % username)
|
||||||
|
# since 4.2.9 login is required for geotags
|
||||||
|
if instaloader.download_geotags and not instaloader.context.is_logged_in:
|
||||||
|
instaloader.context.error("Warning: Use --login to download geotags of posts.")
|
||||||
|
# Try block for KeyboardInterrupt (save session on ^C)
|
||||||
|
profiles = set()
|
||||||
|
anonymous_retry_profiles = set()
|
||||||
|
try:
|
||||||
|
# Generate set of profiles, already downloading non-profile targets
|
||||||
|
for target in targetlist:
|
||||||
|
if (target.endswith('.json') or target.endswith('.json.xz')) and os.path.isfile(target):
|
||||||
|
with instaloader.context.error_catcher(target):
|
||||||
|
structure = load_structure_from_file(instaloader.context, target)
|
||||||
|
if isinstance(structure, Post):
|
||||||
|
if post_filter is not None and not post_filter(structure):
|
||||||
|
instaloader.context.log("<{} ({}) skipped>".format(structure, target), flush=True)
|
||||||
|
continue
|
||||||
|
instaloader.context.log("Downloading {} ({})".format(structure, target))
|
||||||
|
instaloader.download_post(structure, os.path.dirname(target))
|
||||||
|
elif isinstance(structure, StoryItem):
|
||||||
|
if storyitem_filter is not None and not storyitem_filter(structure):
|
||||||
|
instaloader.context.log("<{} ({}) skipped>".format(structure, target), flush=True)
|
||||||
|
continue
|
||||||
|
instaloader.context.log("Attempting to download {} ({})".format(structure, target))
|
||||||
|
instaloader.download_storyitem(structure, os.path.dirname(target))
|
||||||
|
elif isinstance(structure, Profile):
|
||||||
|
raise InvalidArgumentException("Profile JSON are ignored. Pass \"{}\" to download that profile"
|
||||||
|
.format(structure.username))
|
||||||
|
else:
|
||||||
|
raise InvalidArgumentException("{} JSON file not supported as target"
|
||||||
|
.format(structure.__class__.__name__))
|
||||||
|
continue
|
||||||
|
# strip '/' characters to be more shell-autocompletion-friendly
|
||||||
|
target = target.rstrip('/')
|
||||||
|
with instaloader.context.error_catcher(target):
|
||||||
|
if re.match(r"^@[A-Za-z0-9._]+$", target):
|
||||||
|
instaloader.context.log("Retrieving followees of %s..." % target[1:])
|
||||||
|
profile = Profile.from_username(instaloader.context, target[1:])
|
||||||
|
for followee in profile.get_followees():
|
||||||
|
instaloader.save_profile_id(followee)
|
||||||
|
profiles.add(followee)
|
||||||
|
elif re.match(r"^#\w+$", target):
|
||||||
|
instaloader.download_hashtag(hashtag=target[1:], max_count=max_count, fast_update=fast_update,
|
||||||
|
post_filter=post_filter,
|
||||||
|
profile_pic=download_profile_pic, posts=download_posts)
|
||||||
|
elif re.match(r"^-[A-Za-z0-9-_]+$", target):
|
||||||
|
instaloader.download_post(Post.from_shortcode(instaloader.context, target[1:]), target)
|
||||||
|
elif re.match(r"^%[0-9]+$", target):
|
||||||
|
instaloader.download_location(location=target[1:], max_count=max_count, fast_update=fast_update,
|
||||||
|
post_filter=post_filter)
|
||||||
|
elif target == ":feed":
|
||||||
|
instaloader.download_feed_posts(fast_update=fast_update, max_count=max_count,
|
||||||
|
post_filter=post_filter)
|
||||||
|
elif target == ":stories":
|
||||||
|
instaloader.download_stories(fast_update=fast_update, storyitem_filter=storyitem_filter)
|
||||||
|
elif target == ":saved":
|
||||||
|
instaloader.download_saved_posts(fast_update=fast_update, max_count=max_count,
|
||||||
|
post_filter=post_filter)
|
||||||
|
elif re.match(r"^[A-Za-z0-9._]+$", target):
|
||||||
|
try:
|
||||||
|
profile = instaloader.check_profile_id(target)
|
||||||
|
if instaloader.context.is_logged_in and profile.has_blocked_viewer:
|
||||||
|
if download_profile_pic or ((download_posts or download_tagged or download_igtv)
|
||||||
|
and not profile.is_private):
|
||||||
|
raise ProfileNotExistsException("{} blocked you; But we download her anonymously."
|
||||||
|
.format(target))
|
||||||
|
else:
|
||||||
|
instaloader.context.error("{} blocked you.".format(target))
|
||||||
|
else:
|
||||||
|
profiles.add(profile)
|
||||||
|
except ProfileNotExistsException as err:
|
||||||
|
# Not only our profile.has_blocked_viewer condition raises ProfileNotExistsException,
|
||||||
|
# check_profile_id() also does, since access to blocked profile may be responded with 404.
|
||||||
|
if instaloader.context.is_logged_in and (download_profile_pic or download_posts or
|
||||||
|
download_tagged or download_igtv):
|
||||||
|
instaloader.context.log(err)
|
||||||
|
instaloader.context.log("Trying again anonymously, helps in case you are just blocked.")
|
||||||
|
with instaloader.anonymous_copy() as anonymous_loader:
|
||||||
|
with instaloader.context.error_catcher():
|
||||||
|
anonymous_retry_profiles.add(anonymous_loader.check_profile_id(target))
|
||||||
|
instaloader.context.error("Warning: {} will be downloaded anonymously (\"{}\")."
|
||||||
|
.format(target, err))
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
target_type = {
|
||||||
|
'#': 'hashtag',
|
||||||
|
'%': 'location',
|
||||||
|
'-': 'shortcode',
|
||||||
|
}.get(target[0], 'username')
|
||||||
|
raise ProfileNotExistsException('Invalid {} {}'.format(target_type, target))
|
||||||
|
if len(profiles) > 1:
|
||||||
|
instaloader.context.log("Downloading {} profiles: {}".format(len(profiles),
|
||||||
|
' '.join([p.username for p in profiles])))
|
||||||
|
if profiles and (download_profile_pic or download_posts) and not instaloader.context.is_logged_in:
|
||||||
|
instaloader.context.error("Warning: Use --login to download higher-quality versions of pictures.")
|
||||||
|
instaloader.download_profiles(profiles,
|
||||||
|
download_profile_pic, download_posts, download_tagged, download_igtv,
|
||||||
|
download_highlights, download_stories,
|
||||||
|
fast_update, post_filter, storyitem_filter)
|
||||||
|
if anonymous_retry_profiles:
|
||||||
|
instaloader.context.log("Downloading anonymously: {}"
|
||||||
|
.format(' '.join([p.username for p in anonymous_retry_profiles])))
|
||||||
|
with instaloader.anonymous_copy() as anonymous_loader:
|
||||||
|
anonymous_loader.download_profiles(anonymous_retry_profiles,
|
||||||
|
download_profile_pic, download_posts, download_tagged, download_igtv,
|
||||||
|
fast_update=fast_update, post_filter=post_filter)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nInterrupted by user.", file=sys.stderr)
|
||||||
|
# Save session if it is useful
|
||||||
|
if instaloader.context.is_logged_in:
|
||||||
|
instaloader.save_session_to_file(sessionfile)
|
||||||
|
# User might be confused if Instaloader does nothing
|
||||||
|
if not targetlist:
|
||||||
|
if instaloader.context.is_logged_in:
|
||||||
|
# Instaloader did at least save a session file
|
||||||
|
instaloader.context.log("No targets were specified, thus nothing has been downloaded.")
|
||||||
|
else:
|
||||||
|
# Instloader did not do anything
|
||||||
|
instaloader.context.log("usage:" + usage_string())
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = ArgumentParser(description=__doc__, add_help=False, usage=usage_string(),
|
||||||
|
epilog="Report issues at https://github.com/instaloader/instaloader/issues. "
|
||||||
|
"The complete documentation can be found at "
|
||||||
|
"https://instaloader.github.io/.",
|
||||||
|
fromfile_prefix_chars='+')
|
||||||
|
|
||||||
|
g_targets = parser.add_argument_group("What to Download",
|
||||||
|
"Specify a list of targets. For each of these, Instaloader creates a folder "
|
||||||
|
"and downloads all posts. The following targets are supported:")
|
||||||
|
g_targets.add_argument('profile', nargs='*',
|
||||||
|
help="Download profile. If an already-downloaded profile has been renamed, Instaloader "
|
||||||
|
"automatically finds it by its unique ID and renames the folder likewise.")
|
||||||
|
g_targets.add_argument('_at_profile', nargs='*', metavar="@profile",
|
||||||
|
help="Download all followees of profile. Requires --login. "
|
||||||
|
"Consider using :feed rather than @yourself.")
|
||||||
|
g_targets.add_argument('_hashtag', nargs='*', metavar='"#hashtag"', help="Download #hashtag.")
|
||||||
|
g_targets.add_argument('_location', nargs='*', metavar='%location_id',
|
||||||
|
help="Download %%location_id. Requires --login.")
|
||||||
|
g_targets.add_argument('_feed', nargs='*', metavar=":feed",
|
||||||
|
help="Download pictures from your feed. Requires --login.")
|
||||||
|
g_targets.add_argument('_stories', nargs='*', metavar=":stories",
|
||||||
|
help="Download the stories of your followees. Requires --login.")
|
||||||
|
g_targets.add_argument('_saved', nargs='*', metavar=":saved",
|
||||||
|
help="Download the posts that you marked as saved. Requires --login.")
|
||||||
|
g_targets.add_argument('_singlepost', nargs='*', metavar="-- -shortcode",
|
||||||
|
help="Download the post with the given shortcode")
|
||||||
|
g_targets.add_argument('_json', nargs='*', metavar="filename.json[.xz]",
|
||||||
|
help="Re-Download the given object.")
|
||||||
|
g_targets.add_argument('_fromfile', nargs='*', metavar="+args.txt",
|
||||||
|
help="Read targets (and options) from given textfile.")
|
||||||
|
|
||||||
|
g_post = parser.add_argument_group("What to Download of each Post")
|
||||||
|
|
||||||
|
g_prof = parser.add_argument_group("What to Download of each Profile")
|
||||||
|
|
||||||
|
g_prof.add_argument('-P', '--profile-pic-only', action='store_true',
|
||||||
|
help=SUPPRESS)
|
||||||
|
g_prof.add_argument('--no-posts', action='store_true',
|
||||||
|
help="Do not download regular posts.")
|
||||||
|
g_prof.add_argument('--no-profile-pic', action='store_true',
|
||||||
|
help='Do not download profile picture.')
|
||||||
|
g_post.add_argument('--no-pictures', action='store_true',
|
||||||
|
help='Do not download post pictures. Cannot be used together with --fast-update. '
|
||||||
|
'Implies --no-video-thumbnails, does not imply --no-videos.')
|
||||||
|
g_post.add_argument('-V', '--no-videos', action='store_true',
|
||||||
|
help='Do not download videos.')
|
||||||
|
g_post.add_argument('--no-video-thumbnails', action='store_true',
|
||||||
|
help='Do not download thumbnails of videos.')
|
||||||
|
g_post.add_argument('-G', '--geotags', action='store_true',
|
||||||
|
help='Download geotags when available. Geotags are stored as a '
|
||||||
|
'text file with the location\'s name and a Google Maps link. '
|
||||||
|
'This requires an additional request to the Instagram '
|
||||||
|
'server for each picture. Requires --login.')
|
||||||
|
g_post.add_argument('-C', '--comments', action='store_true',
|
||||||
|
help='Download and update comments for each post. '
|
||||||
|
'This requires an additional request to the Instagram '
|
||||||
|
'server for each post, which is why it is disabled by default.')
|
||||||
|
g_post.add_argument('--no-captions', action='store_true',
|
||||||
|
help='Do not create txt files.')
|
||||||
|
g_post.add_argument('--post-metadata-txt', action='append',
|
||||||
|
help='Template to write in txt file for each Post.')
|
||||||
|
g_post.add_argument('--storyitem-metadata-txt', action='append',
|
||||||
|
help='Template to write in txt file for each StoryItem.')
|
||||||
|
g_post.add_argument('--no-metadata-json', action='store_true',
|
||||||
|
help='Do not create a JSON file containing the metadata of each post.')
|
||||||
|
g_post.add_argument('--metadata-json', action='store_true',
|
||||||
|
help=SUPPRESS)
|
||||||
|
g_post.add_argument('--no-compress-json', action='store_true',
|
||||||
|
help='Do not xz compress JSON files, rather create pretty formatted JSONs.')
|
||||||
|
g_prof.add_argument('-s', '--stories', action='store_true',
|
||||||
|
help='Also download stories of each profile that is downloaded. Requires --login.')
|
||||||
|
g_prof.add_argument('--stories-only', action='store_true',
|
||||||
|
help=SUPPRESS)
|
||||||
|
g_prof.add_argument('--highlights', action='store_true',
|
||||||
|
help='Also download highlights of each profile that is downloaded. Requires --login.')
|
||||||
|
g_prof.add_argument('--tagged', action='store_true',
|
||||||
|
help='Also download posts where each profile is tagged.')
|
||||||
|
g_prof.add_argument('--igtv', action='store_true',
|
||||||
|
help='Also download IGTV videos.')
|
||||||
|
|
||||||
|
g_cond = parser.add_argument_group("Which Posts to Download")
|
||||||
|
|
||||||
|
g_cond.add_argument('-F', '--fast-update', action='store_true',
|
||||||
|
help='For each target, stop when encountering the first already-downloaded picture. This '
|
||||||
|
'flag is recommended when you use Instaloader to update your personal Instagram archive.')
|
||||||
|
|
||||||
|
g_cond.add_argument('--post-filter', '--only-if', metavar='filter',
|
||||||
|
help='Expression that, if given, must evaluate to True for each post to be downloaded. Must be '
|
||||||
|
'a syntactically valid python expression. Variables are evaluated to '
|
||||||
|
'instaloader.Post attributes. Example: --post-filter=viewer_has_liked.')
|
||||||
|
g_cond.add_argument('--storyitem-filter', metavar='filter',
|
||||||
|
help='Expression that, if given, must evaluate to True for each storyitem to be downloaded. '
|
||||||
|
'Must be a syntactically valid python expression. Variables are evaluated to '
|
||||||
|
'instaloader.StoryItem attributes.')
|
||||||
|
|
||||||
|
g_cond.add_argument('-c', '--count',
|
||||||
|
help='Do not attempt to download more than COUNT posts. '
|
||||||
|
'Applies only to #hashtag and :feed.')
|
||||||
|
|
||||||
|
g_login = parser.add_argument_group('Login (Download Private Profiles)',
|
||||||
|
'Instaloader can login to Instagram. This allows downloading private profiles. '
|
||||||
|
'To login, pass the --login option. Your session cookie (not your password!) '
|
||||||
|
'will be saved to a local file to be reused next time you want Instaloader '
|
||||||
|
'to login.')
|
||||||
|
g_login.add_argument('-l', '--login', metavar='YOUR-USERNAME',
|
||||||
|
help='Login name (profile name) for your Instagram account.')
|
||||||
|
g_login.add_argument('-f', '--sessionfile',
|
||||||
|
help='Path for loading and storing session key file. '
|
||||||
|
'Defaults to ' + get_default_session_filename("<login_name>"))
|
||||||
|
g_login.add_argument('-p', '--password', metavar='YOUR-PASSWORD',
|
||||||
|
help='Password for your Instagram account. Without this option, '
|
||||||
|
'you\'ll be prompted for your password interactively if '
|
||||||
|
'there is not yet a valid session file.')
|
||||||
|
|
||||||
|
g_how = parser.add_argument_group('How to Download')
|
||||||
|
g_how.add_argument('--dirname-pattern',
|
||||||
|
help='Name of directory where to store posts. {profile} is replaced by the profile name, '
|
||||||
|
'{target} is replaced by the target you specified, i.e. either :feed, #hashtag or the '
|
||||||
|
'profile name. Defaults to \'{target}\'.')
|
||||||
|
g_how.add_argument('--filename-pattern',
|
||||||
|
help='Prefix of filenames, relative to the directory given with '
|
||||||
|
'--dirname-pattern. {profile} is replaced by the profile name,'
|
||||||
|
'{target} is replaced by the target you specified, i.e. either :feed'
|
||||||
|
'#hashtag or the profile name. Defaults to \'{date_utc}_UTC\'')
|
||||||
|
g_how.add_argument('--resume-prefix', metavar='PREFIX',
|
||||||
|
help='Prefix for filenames that are used to save the information to resume an interrupted '
|
||||||
|
'download.')
|
||||||
|
g_how.add_argument('--no-resume', action='store_true',
|
||||||
|
help='Do not resume a previously-aborted download iteration, and do not save such information '
|
||||||
|
'when interrupted.')
|
||||||
|
g_how.add_argument('--use-aged-resume-files', action='store_true', help=SUPPRESS)
|
||||||
|
g_how.add_argument('--user-agent',
|
||||||
|
help='User Agent to use for HTTP requests. Defaults to \'{}\'.'.format(default_user_agent()))
|
||||||
|
g_how.add_argument('-S', '--no-sleep', action='store_true', help=SUPPRESS)
|
||||||
|
g_how.add_argument('--max-connection-attempts', metavar='N', type=int, default=3,
|
||||||
|
help='Maximum number of connection attempts until a request is aborted. Defaults to 3. If a '
|
||||||
|
'connection fails, it can be manually skipped by hitting CTRL+C. Set this to 0 to retry '
|
||||||
|
'infinitely.')
|
||||||
|
g_how.add_argument('--commit-mode', action='store_true', help=SUPPRESS)
|
||||||
|
g_how.add_argument('--request-timeout', metavar='N', type=float,
|
||||||
|
help='seconds to wait before timing out a connection request')
|
||||||
|
|
||||||
|
g_misc = parser.add_argument_group('Miscellaneous Options')
|
||||||
|
g_misc.add_argument('-q', '--quiet', action='store_true',
|
||||||
|
help='Disable user interaction, i.e. do not print messages (except errors) and fail '
|
||||||
|
'if login credentials are needed but not given. This makes Instaloader suitable as a '
|
||||||
|
'cron job.')
|
||||||
|
g_misc.add_argument('-h', '--help', action='help', help='Show this help message and exit.')
|
||||||
|
g_misc.add_argument('--version', action='version', help='Show version number and exit.',
|
||||||
|
version=__version__)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
try:
|
||||||
|
if args.login is None and (args.stories or args.stories_only):
|
||||||
|
print("--login=USERNAME required to download stories.", file=sys.stderr)
|
||||||
|
args.stories = False
|
||||||
|
if args.stories_only:
|
||||||
|
raise SystemExit(1)
|
||||||
|
|
||||||
|
if ':feed-all' in args.profile or ':feed-liked' in args.profile:
|
||||||
|
raise SystemExit(":feed-all and :feed-liked were removed. Use :feed as target and "
|
||||||
|
"eventually --post-filter=viewer_has_liked.")
|
||||||
|
|
||||||
|
post_metadata_txt_pattern = '\n'.join(args.post_metadata_txt) if args.post_metadata_txt else None
|
||||||
|
storyitem_metadata_txt_pattern = '\n'.join(args.storyitem_metadata_txt) if args.storyitem_metadata_txt else None
|
||||||
|
|
||||||
|
if args.no_captions:
|
||||||
|
if not (post_metadata_txt_pattern or storyitem_metadata_txt_pattern):
|
||||||
|
post_metadata_txt_pattern = ''
|
||||||
|
storyitem_metadata_txt_pattern = ''
|
||||||
|
else:
|
||||||
|
raise SystemExit("--no-captions and --post-metadata-txt or --storyitem-metadata-txt given; "
|
||||||
|
"That contradicts.")
|
||||||
|
|
||||||
|
if args.no_resume and args.resume_prefix:
|
||||||
|
raise SystemExit("--no-resume and --resume-prefix given; That contradicts.")
|
||||||
|
resume_prefix = (args.resume_prefix if args.resume_prefix else 'iterator') if not args.no_resume else None
|
||||||
|
|
||||||
|
if args.no_pictures and args.fast_update:
|
||||||
|
raise SystemExit('--no-pictures and --fast-update cannot be used together.')
|
||||||
|
|
||||||
|
# Determine what to download
|
||||||
|
download_profile_pic = not args.no_profile_pic or args.profile_pic_only
|
||||||
|
download_posts = not (args.no_posts or args.stories_only or args.profile_pic_only)
|
||||||
|
download_stories = args.stories or args.stories_only
|
||||||
|
|
||||||
|
loader = Instaloader(sleep=not args.no_sleep, quiet=args.quiet, user_agent=args.user_agent,
|
||||||
|
dirname_pattern=args.dirname_pattern, filename_pattern=args.filename_pattern,
|
||||||
|
download_pictures=not args.no_pictures,
|
||||||
|
download_videos=not args.no_videos, download_video_thumbnails=not args.no_video_thumbnails,
|
||||||
|
download_geotags=args.geotags,
|
||||||
|
download_comments=args.comments, save_metadata=not args.no_metadata_json,
|
||||||
|
compress_json=not args.no_compress_json,
|
||||||
|
post_metadata_txt_pattern=post_metadata_txt_pattern,
|
||||||
|
storyitem_metadata_txt_pattern=storyitem_metadata_txt_pattern,
|
||||||
|
max_connection_attempts=args.max_connection_attempts,
|
||||||
|
request_timeout=args.request_timeout,
|
||||||
|
resume_prefix=resume_prefix,
|
||||||
|
check_resume_bbd=not args.use_aged_resume_files)
|
||||||
|
_main(loader,
|
||||||
|
args.profile,
|
||||||
|
username=args.login.lower() if args.login is not None else None,
|
||||||
|
password=args.password,
|
||||||
|
sessionfile=args.sessionfile,
|
||||||
|
download_profile_pic=download_profile_pic,
|
||||||
|
download_posts=download_posts,
|
||||||
|
download_stories=download_stories,
|
||||||
|
download_highlights=args.highlights,
|
||||||
|
download_tagged=args.tagged,
|
||||||
|
download_igtv=args.igtv,
|
||||||
|
fast_update=args.fast_update,
|
||||||
|
max_count=int(args.count) if args.count is not None else None,
|
||||||
|
post_filter_str=args.post_filter,
|
||||||
|
storyitem_filter_str=args.storyitem_filter)
|
||||||
|
loader.close()
|
||||||
|
except InstaloaderException as err:
|
||||||
|
raise SystemExit("Fatal error: %s" % err)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
class InstaloaderException(Exception):
|
||||||
|
"""Base exception for this script.
|
||||||
|
|
||||||
|
:note: This exception should not be raised directly."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class QueryReturnedBadRequestException(InstaloaderException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class QueryReturnedForbiddenException(InstaloaderException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileNotExistsException(InstaloaderException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileHasNoPicsException(InstaloaderException):
|
||||||
|
"""
|
||||||
|
.. deprecated:: 4.2.2
|
||||||
|
Not raised anymore.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PrivateProfileNotFollowedException(InstaloaderException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LoginRequiredException(InstaloaderException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TwoFactorAuthRequiredException(InstaloaderException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidArgumentException(InstaloaderException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BadResponseException(InstaloaderException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BadCredentialsException(InstaloaderException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionException(InstaloaderException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PostChangedException(InstaloaderException):
|
||||||
|
""".. versionadded:: 4.2.2"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class QueryReturnedNotFoundException(ConnectionException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TooManyRequestsException(ConnectionException):
|
||||||
|
pass
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,633 @@
|
|||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pickle
|
||||||
|
import random
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import textwrap
|
||||||
|
import time
|
||||||
|
import urllib.parse
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from functools import partial
|
||||||
|
from typing import Any, Callable, Dict, Iterator, List, Optional, Union
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import requests.utils
|
||||||
|
|
||||||
|
from .exceptions import *
|
||||||
|
|
||||||
|
|
||||||
|
def copy_session(session: requests.Session, request_timeout: Optional[float] = None) -> requests.Session:
|
||||||
|
"""Duplicates a requests.Session."""
|
||||||
|
new = requests.Session()
|
||||||
|
new.cookies = requests.utils.cookiejar_from_dict(requests.utils.dict_from_cookiejar(session.cookies))
|
||||||
|
new.headers = session.headers.copy()
|
||||||
|
if request_timeout is not None:
|
||||||
|
# Override default timeout behavior.
|
||||||
|
# Need to silence mypy bug for this. See: https://github.com/python/mypy/issues/2427
|
||||||
|
new.request = partial(new.request, timeout=request_timeout) # type: ignore
|
||||||
|
return new
|
||||||
|
|
||||||
|
|
||||||
|
def default_user_agent() -> str:
|
||||||
|
return 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' \
|
||||||
|
'(KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36'
|
||||||
|
|
||||||
|
|
||||||
|
class InstaloaderContext:
|
||||||
|
"""Class providing methods for (error) logging and low-level communication with Instagram.
|
||||||
|
|
||||||
|
It is not thought to be instantiated directly, rather :class:`Instaloader` instances maintain a context
|
||||||
|
object.
|
||||||
|
|
||||||
|
For logging, it provides :meth:`log`, :meth:`error`, :meth:`error_catcher`.
|
||||||
|
|
||||||
|
It provides low-level communication routines :meth:`get_json`, :meth:`graphql_query`, :meth:`graphql_node_list`,
|
||||||
|
:meth:`get_and_write_raw` and implements mechanisms for rate controlling and error handling.
|
||||||
|
|
||||||
|
Further, it provides methods for logging in and general session handles, which are used by that routines in
|
||||||
|
class :class:`Instaloader`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, sleep: bool = True, quiet: bool = False, user_agent: Optional[str] = None,
|
||||||
|
max_connection_attempts: int = 3, request_timeout: Optional[float] = None,
|
||||||
|
rate_controller: Optional[Callable[["InstaloaderContext"], "RateController"]] = None):
|
||||||
|
|
||||||
|
self.user_agent = user_agent if user_agent is not None else default_user_agent()
|
||||||
|
self.request_timeout = request_timeout
|
||||||
|
self._session = self.get_anonymous_session()
|
||||||
|
self.username = None
|
||||||
|
self.sleep = sleep
|
||||||
|
self.quiet = quiet
|
||||||
|
self.max_connection_attempts = max_connection_attempts
|
||||||
|
self._graphql_page_length = 50
|
||||||
|
self._root_rhx_gis = None
|
||||||
|
self.two_factor_auth_pending = None
|
||||||
|
|
||||||
|
# error log, filled with error() and printed at the end of Instaloader.main()
|
||||||
|
self.error_log = [] # type: List[str]
|
||||||
|
|
||||||
|
self._rate_controller = rate_controller(self) if rate_controller is not None else RateController(self)
|
||||||
|
|
||||||
|
# Can be set to True for testing, disables supression of InstaloaderContext._error_catcher
|
||||||
|
self.raise_all_errors = False
|
||||||
|
|
||||||
|
# Cache profile from id (mapping from id to Profile)
|
||||||
|
self.profile_id_cache = dict() # type: Dict[int, Any]
|
||||||
|
|
||||||
|
# Cache profile from name (mapping from username to Profile)
|
||||||
|
self.profile_name_cache = dict() # type: Dict[str, Any]
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def anonymous_copy(self):
|
||||||
|
session = self._session
|
||||||
|
username = self.username
|
||||||
|
self._session = self.get_anonymous_session()
|
||||||
|
self.username = None
|
||||||
|
try:
|
||||||
|
yield self
|
||||||
|
finally:
|
||||||
|
self._session.close()
|
||||||
|
self.username = username
|
||||||
|
self._session = session
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_logged_in(self) -> bool:
|
||||||
|
"""True, if this Instaloader instance is logged in."""
|
||||||
|
return bool(self.username)
|
||||||
|
|
||||||
|
def log(self, *msg, sep='', end='\n', flush=False):
|
||||||
|
"""Log a message to stdout that can be suppressed with --quiet."""
|
||||||
|
if not self.quiet:
|
||||||
|
print(*msg, sep=sep, end=end, flush=flush)
|
||||||
|
|
||||||
|
def error(self, msg, repeat_at_end=True):
|
||||||
|
"""Log a non-fatal error message to stderr, which is repeated at program termination.
|
||||||
|
|
||||||
|
:param msg: Message to be printed.
|
||||||
|
:param repeat_at_end: Set to false if the message should be printed, but not repeated at program termination."""
|
||||||
|
print(msg, file=sys.stderr)
|
||||||
|
if repeat_at_end:
|
||||||
|
self.error_log.append(msg)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Print error log and close session"""
|
||||||
|
if self.error_log and not self.quiet:
|
||||||
|
print("\nErrors or warnings occurred:", file=sys.stderr)
|
||||||
|
for err in self.error_log:
|
||||||
|
print(err, file=sys.stderr)
|
||||||
|
self._session.close()
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def error_catcher(self, extra_info: Optional[str] = None):
|
||||||
|
"""
|
||||||
|
Context manager to catch, print and record InstaloaderExceptions.
|
||||||
|
|
||||||
|
:param extra_info: String to prefix error message with."""
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except InstaloaderException as err:
|
||||||
|
if extra_info:
|
||||||
|
self.error('{}: {}'.format(extra_info, err))
|
||||||
|
else:
|
||||||
|
self.error('{}'.format(err))
|
||||||
|
if self.raise_all_errors:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _default_http_header(self, empty_session_only: bool = False) -> Dict[str, str]:
|
||||||
|
"""Returns default HTTP header we use for requests."""
|
||||||
|
header = {'Accept-Encoding': 'gzip, deflate',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.8',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
'Content-Length': '0',
|
||||||
|
'Host': 'www.instagram.com',
|
||||||
|
'Origin': 'https://www.instagram.com',
|
||||||
|
'Referer': 'https://www.instagram.com/',
|
||||||
|
'User-Agent': self.user_agent,
|
||||||
|
'X-Instagram-AJAX': '1',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'}
|
||||||
|
if empty_session_only:
|
||||||
|
del header['Host']
|
||||||
|
del header['Origin']
|
||||||
|
del header['Referer']
|
||||||
|
del header['X-Instagram-AJAX']
|
||||||
|
del header['X-Requested-With']
|
||||||
|
return header
|
||||||
|
|
||||||
|
def get_anonymous_session(self) -> requests.Session:
|
||||||
|
"""Returns our default anonymous requests.Session object."""
|
||||||
|
session = requests.Session()
|
||||||
|
session.cookies.update({'sessionid': '', 'mid': '', 'ig_pr': '1',
|
||||||
|
'ig_vw': '1920', 'csrftoken': '',
|
||||||
|
's_network': '', 'ds_user_id': ''})
|
||||||
|
session.headers.update(self._default_http_header(empty_session_only=True))
|
||||||
|
if self.request_timeout is not None:
|
||||||
|
# Override default timeout behavior.
|
||||||
|
# Need to silence mypy bug for this. See: https://github.com/python/mypy/issues/2427
|
||||||
|
session.request = partial(session.request, timeout=self.request_timeout) # type: ignore
|
||||||
|
return session
|
||||||
|
|
||||||
|
def save_session_to_file(self, sessionfile):
|
||||||
|
"""Not meant to be used directly, use :meth:`Instaloader.save_session_to_file`."""
|
||||||
|
pickle.dump(requests.utils.dict_from_cookiejar(self._session.cookies), sessionfile)
|
||||||
|
|
||||||
|
def load_session_from_file(self, username, sessionfile):
|
||||||
|
"""Not meant to be used directly, use :meth:`Instaloader.load_session_from_file`."""
|
||||||
|
session = requests.Session()
|
||||||
|
session.cookies = requests.utils.cookiejar_from_dict(pickle.load(sessionfile))
|
||||||
|
session.headers.update(self._default_http_header())
|
||||||
|
session.headers.update({'X-CSRFToken': session.cookies.get_dict()['csrftoken']})
|
||||||
|
if self.request_timeout is not None:
|
||||||
|
# Override default timeout behavior.
|
||||||
|
# Need to silence mypy bug for this. See: https://github.com/python/mypy/issues/2427
|
||||||
|
session.request = partial(session.request, timeout=self.request_timeout) # type: ignore
|
||||||
|
self._session = session
|
||||||
|
self.username = username
|
||||||
|
|
||||||
|
def test_login(self) -> Optional[str]:
|
||||||
|
"""Not meant to be used directly, use :meth:`Instaloader.test_login`.
|
||||||
|
|
||||||
|
.. deprecated:: 4.5
|
||||||
|
:meth:`Instaloader.test_login` now calls :meth:`InstaloaderContext.graphql_query` directly
|
||||||
|
without using this function.
|
||||||
|
"""
|
||||||
|
data = self.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {})
|
||||||
|
return data["data"]["user"]["username"] if data["data"]["user"] is not None else None
|
||||||
|
|
||||||
|
def login(self, user, passwd):
|
||||||
|
"""Not meant to be used directly, use :meth:`Instaloader.login`.
|
||||||
|
|
||||||
|
:raises InvalidArgumentException: If the provided username does not exist.
|
||||||
|
:raises BadCredentialsException: If the provided password is wrong.
|
||||||
|
:raises ConnectionException: If connection to Instagram failed.
|
||||||
|
:raises TwoFactorAuthRequiredException: First step of 2FA login done, now call
|
||||||
|
:meth:`Instaloader.two_factor_login`."""
|
||||||
|
# pylint:disable=import-outside-toplevel
|
||||||
|
import http.client
|
||||||
|
# pylint:disable=protected-access
|
||||||
|
http.client._MAXHEADERS = 200
|
||||||
|
session = requests.Session()
|
||||||
|
session.cookies.update({'sessionid': '', 'mid': '', 'ig_pr': '1',
|
||||||
|
'ig_vw': '1920', 'ig_cb': '1', 'csrftoken': '',
|
||||||
|
's_network': '', 'ds_user_id': ''})
|
||||||
|
session.headers.update(self._default_http_header())
|
||||||
|
if self.request_timeout is not None:
|
||||||
|
# Override default timeout behavior.
|
||||||
|
# Need to silence mypy bug for this. See: https://github.com/python/mypy/issues/2427
|
||||||
|
session.request = partial(session.request, timeout=self.request_timeout) # type: ignore
|
||||||
|
session.get('https://www.instagram.com/web/__mid/')
|
||||||
|
csrf_token = session.cookies.get_dict()['csrftoken']
|
||||||
|
session.headers.update({'X-CSRFToken': csrf_token})
|
||||||
|
# Not using self.get_json() here, because we need to access csrftoken cookie
|
||||||
|
self.do_sleep()
|
||||||
|
# Workaround credits to pgrimaud.
|
||||||
|
# See: https://github.com/pgrimaud/instagram-user-feed/commit/96ad4cf54d1ad331b337f325c73e664999a6d066
|
||||||
|
enc_password = '#PWD_INSTAGRAM_BROWSER:0:{}:{}'.format(int(datetime.now().timestamp()), passwd)
|
||||||
|
login = session.post('https://www.instagram.com/accounts/login/ajax/',
|
||||||
|
data={'enc_password': enc_password, 'username': user}, allow_redirects=True)
|
||||||
|
try:
|
||||||
|
resp_json = login.json()
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
raise ConnectionException("Login error: JSON decode fail, {} - {}.".format(login.status_code, login.reason))
|
||||||
|
if resp_json.get('two_factor_required'):
|
||||||
|
two_factor_session = copy_session(session, self.request_timeout)
|
||||||
|
two_factor_session.headers.update({'X-CSRFToken': csrf_token})
|
||||||
|
two_factor_session.cookies.update({'csrftoken': csrf_token})
|
||||||
|
self.two_factor_auth_pending = (two_factor_session,
|
||||||
|
user,
|
||||||
|
resp_json['two_factor_info']['two_factor_identifier'])
|
||||||
|
raise TwoFactorAuthRequiredException("Login error: two-factor authentication required.")
|
||||||
|
if resp_json.get('checkpoint_url'):
|
||||||
|
raise ConnectionException("Login: Checkpoint required. Point your browser to "
|
||||||
|
"https://www.instagram.com{} - "
|
||||||
|
"follow the instructions, then retry.".format(resp_json.get('checkpoint_url')))
|
||||||
|
if resp_json['status'] != 'ok':
|
||||||
|
if 'message' in resp_json:
|
||||||
|
raise ConnectionException("Login error: \"{}\" status, message \"{}\".".format(resp_json['status'],
|
||||||
|
resp_json['message']))
|
||||||
|
else:
|
||||||
|
raise ConnectionException("Login error: \"{}\" status.".format(resp_json['status']))
|
||||||
|
if 'authenticated' not in resp_json:
|
||||||
|
# Issue #472
|
||||||
|
if 'message' in resp_json:
|
||||||
|
raise ConnectionException("Login error: Unexpected response, \"{}\".".format(resp_json['message']))
|
||||||
|
else:
|
||||||
|
raise ConnectionException("Login error: Unexpected response, this might indicate a blocked IP.")
|
||||||
|
if not resp_json['authenticated']:
|
||||||
|
if resp_json['user']:
|
||||||
|
# '{"authenticated": false, "user": true, "status": "ok"}'
|
||||||
|
raise BadCredentialsException('Login error: Wrong password.')
|
||||||
|
else:
|
||||||
|
# '{"authenticated": false, "user": false, "status": "ok"}'
|
||||||
|
# Raise InvalidArgumentException rather than BadCredentialException, because BadCredentialException
|
||||||
|
# triggers re-asking of password in Instaloader.interactive_login(), which makes no sense if the
|
||||||
|
# username is invalid.
|
||||||
|
raise InvalidArgumentException('Login error: User {} does not exist.'.format(user))
|
||||||
|
# '{"authenticated": true, "user": true, "userId": ..., "oneTapPrompt": false, "status": "ok"}'
|
||||||
|
session.headers.update({'X-CSRFToken': login.cookies['csrftoken']})
|
||||||
|
self._session = session
|
||||||
|
self.username = user
|
||||||
|
|
||||||
|
def two_factor_login(self, two_factor_code):
|
||||||
|
"""Second step of login if 2FA is enabled.
|
||||||
|
Not meant to be used directly, use :meth:`Instaloader.two_factor_login`.
|
||||||
|
|
||||||
|
:raises InvalidArgumentException: No two-factor authentication pending.
|
||||||
|
:raises BadCredentialsException: 2FA verification code invalid.
|
||||||
|
|
||||||
|
.. versionadded:: 4.2"""
|
||||||
|
if not self.two_factor_auth_pending:
|
||||||
|
raise InvalidArgumentException("No two-factor authentication pending.")
|
||||||
|
(session, user, two_factor_id) = self.two_factor_auth_pending
|
||||||
|
|
||||||
|
login = session.post('https://www.instagram.com/accounts/login/ajax/two_factor/',
|
||||||
|
data={'username': user, 'verificationCode': two_factor_code, 'identifier': two_factor_id},
|
||||||
|
allow_redirects=True)
|
||||||
|
resp_json = login.json()
|
||||||
|
if resp_json['status'] != 'ok':
|
||||||
|
if 'message' in resp_json:
|
||||||
|
raise BadCredentialsException("Login error: {}".format(resp_json['message']))
|
||||||
|
else:
|
||||||
|
raise BadCredentialsException("Login error: \"{}\" status.".format(resp_json['status']))
|
||||||
|
session.headers.update({'X-CSRFToken': login.cookies['csrftoken']})
|
||||||
|
self._session = session
|
||||||
|
self.username = user
|
||||||
|
self.two_factor_auth_pending = None
|
||||||
|
|
||||||
|
def do_sleep(self):
|
||||||
|
"""Sleep a short time if self.sleep is set. Called before each request to instagram.com."""
|
||||||
|
if self.sleep:
|
||||||
|
time.sleep(min(random.expovariate(0.7), 5.0))
|
||||||
|
|
||||||
|
def get_json(self, path: str, params: Dict[str, Any], host: str = 'www.instagram.com',
|
||||||
|
session: Optional[requests.Session] = None, _attempt=1) -> Dict[str, Any]:
|
||||||
|
"""JSON request to Instagram.
|
||||||
|
|
||||||
|
:param path: URL, relative to the given domain which defaults to www.instagram.com/
|
||||||
|
:param params: GET parameters
|
||||||
|
:param host: Domain part of the URL from where to download the requested JSON; defaults to www.instagram.com
|
||||||
|
:param session: Session to use, or None to use self.session
|
||||||
|
:return: Decoded response dictionary
|
||||||
|
:raises QueryReturnedBadRequestException: When the server responds with a 400.
|
||||||
|
:raises QueryReturnedNotFoundException: When the server responds with a 404.
|
||||||
|
:raises ConnectionException: When query repeatedly failed.
|
||||||
|
"""
|
||||||
|
is_graphql_query = 'query_hash' in params and 'graphql/query' in path
|
||||||
|
is_iphone_query = host == 'i.instagram.com'
|
||||||
|
is_other_query = not is_graphql_query and host == "www.instagram.com"
|
||||||
|
sess = session if session else self._session
|
||||||
|
try:
|
||||||
|
self.do_sleep()
|
||||||
|
if is_graphql_query:
|
||||||
|
self._rate_controller.wait_before_query(params['query_hash'])
|
||||||
|
if is_iphone_query:
|
||||||
|
self._rate_controller.wait_before_query('iphone')
|
||||||
|
if is_other_query:
|
||||||
|
self._rate_controller.wait_before_query('other')
|
||||||
|
resp = sess.get('https://{0}/{1}'.format(host, path), params=params, allow_redirects=False)
|
||||||
|
while resp.is_redirect:
|
||||||
|
redirect_url = resp.headers['location']
|
||||||
|
self.log('\nHTTP redirect from https://{0}/{1} to {2}'.format(host, path, redirect_url))
|
||||||
|
if redirect_url.startswith('https://www.instagram.com/accounts/login'):
|
||||||
|
# alternate rate limit exceeded behavior
|
||||||
|
raise TooManyRequestsException("429 Too Many Requests: redirected to login")
|
||||||
|
if redirect_url.startswith('https://{}/'.format(host)):
|
||||||
|
resp = sess.get(redirect_url if redirect_url.endswith('/') else redirect_url + '/',
|
||||||
|
params=params, allow_redirects=False)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
if resp.status_code == 400:
|
||||||
|
raise QueryReturnedBadRequestException("400 Bad Request")
|
||||||
|
if resp.status_code == 404:
|
||||||
|
raise QueryReturnedNotFoundException("404 Not Found")
|
||||||
|
if resp.status_code == 429:
|
||||||
|
raise TooManyRequestsException("429 Too Many Requests")
|
||||||
|
if resp.status_code != 200:
|
||||||
|
raise ConnectionException("HTTP error code {}.".format(resp.status_code))
|
||||||
|
is_html_query = not is_graphql_query and not "__a" in params and host == "www.instagram.com"
|
||||||
|
if is_html_query:
|
||||||
|
match = re.search(r'window\._sharedData = (.*);</script>', resp.text)
|
||||||
|
if match is None:
|
||||||
|
raise QueryReturnedNotFoundException("Could not find \"window._sharedData\" in html response.")
|
||||||
|
resp_json = json.loads(match.group(1))
|
||||||
|
entry_data = resp_json.get('entry_data')
|
||||||
|
post_or_profile_page = list(entry_data.values())[0] if entry_data is not None else None
|
||||||
|
if post_or_profile_page is None:
|
||||||
|
raise ConnectionException("\"window._sharedData\" does not contain required keys.")
|
||||||
|
# If GraphQL data is missing in `window._sharedData`, search for it in `__additionalDataLoaded`.
|
||||||
|
if 'graphql' not in post_or_profile_page[0]:
|
||||||
|
match = re.search(r'window\.__additionalDataLoaded\([^{]+{"graphql":({.*})}\);</script>',
|
||||||
|
resp.text)
|
||||||
|
if match is not None:
|
||||||
|
post_or_profile_page[0]['graphql'] = json.loads(match.group(1))
|
||||||
|
return resp_json
|
||||||
|
else:
|
||||||
|
resp_json = resp.json()
|
||||||
|
if 'status' in resp_json and resp_json['status'] != "ok":
|
||||||
|
if 'message' in resp_json:
|
||||||
|
raise ConnectionException("Returned \"{}\" status, message \"{}\".".format(resp_json['status'],
|
||||||
|
resp_json['message']))
|
||||||
|
else:
|
||||||
|
raise ConnectionException("Returned \"{}\" status.".format(resp_json['status']))
|
||||||
|
return resp_json
|
||||||
|
except (ConnectionException, json.decoder.JSONDecodeError, requests.exceptions.RequestException) as err:
|
||||||
|
error_string = "JSON Query to {}: {}".format(path, err)
|
||||||
|
if _attempt == self.max_connection_attempts:
|
||||||
|
if isinstance(err, QueryReturnedNotFoundException):
|
||||||
|
raise QueryReturnedNotFoundException(error_string) from err
|
||||||
|
else:
|
||||||
|
raise ConnectionException(error_string) from err
|
||||||
|
self.error(error_string + " [retrying; skip with ^C]", repeat_at_end=False)
|
||||||
|
try:
|
||||||
|
if isinstance(err, TooManyRequestsException):
|
||||||
|
if is_graphql_query:
|
||||||
|
self._rate_controller.handle_429(params['query_hash'])
|
||||||
|
if is_iphone_query:
|
||||||
|
self._rate_controller.handle_429('iphone')
|
||||||
|
if is_other_query:
|
||||||
|
self._rate_controller.handle_429('other')
|
||||||
|
return self.get_json(path=path, params=params, host=host, session=sess, _attempt=_attempt + 1)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self.error("[skipped by user]", repeat_at_end=False)
|
||||||
|
raise ConnectionException(error_string) from err
|
||||||
|
|
||||||
|
def graphql_query(self, query_hash: str, variables: Dict[str, Any],
|
||||||
|
referer: Optional[str] = None, rhx_gis: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Do a GraphQL Query.
|
||||||
|
|
||||||
|
:param query_hash: Query identifying hash.
|
||||||
|
:param variables: Variables for the Query.
|
||||||
|
:param referer: HTTP Referer, or None.
|
||||||
|
:param rhx_gis: 'rhx_gis' variable as somewhere returned by Instagram, needed to 'sign' request
|
||||||
|
:return: The server's response dictionary.
|
||||||
|
"""
|
||||||
|
with copy_session(self._session, self.request_timeout) as tmpsession:
|
||||||
|
tmpsession.headers.update(self._default_http_header(empty_session_only=True))
|
||||||
|
del tmpsession.headers['Connection']
|
||||||
|
del tmpsession.headers['Content-Length']
|
||||||
|
tmpsession.headers['authority'] = 'www.instagram.com'
|
||||||
|
tmpsession.headers['scheme'] = 'https'
|
||||||
|
tmpsession.headers['accept'] = '*/*'
|
||||||
|
if referer is not None:
|
||||||
|
tmpsession.headers['referer'] = urllib.parse.quote(referer)
|
||||||
|
|
||||||
|
variables_json = json.dumps(variables, separators=(',', ':'))
|
||||||
|
|
||||||
|
if rhx_gis:
|
||||||
|
#self.log("rhx_gis {} query_hash {}".format(rhx_gis, query_hash))
|
||||||
|
values = "{}:{}".format(rhx_gis, variables_json)
|
||||||
|
x_instagram_gis = hashlib.md5(values.encode()).hexdigest()
|
||||||
|
tmpsession.headers['x-instagram-gis'] = x_instagram_gis
|
||||||
|
|
||||||
|
resp_json = self.get_json('graphql/query',
|
||||||
|
params={'query_hash': query_hash,
|
||||||
|
'variables': variables_json},
|
||||||
|
session=tmpsession)
|
||||||
|
if 'status' not in resp_json:
|
||||||
|
self.error("GraphQL response did not contain a \"status\" field.")
|
||||||
|
return resp_json
|
||||||
|
|
||||||
|
def graphql_node_list(self, query_hash: str, query_variables: Dict[str, Any],
|
||||||
|
query_referer: Optional[str],
|
||||||
|
edge_extractor: Callable[[Dict[str, Any]], Dict[str, Any]],
|
||||||
|
rhx_gis: Optional[str] = None,
|
||||||
|
first_data: Optional[Dict[str, Any]] = None) -> Iterator[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Retrieve a list of GraphQL nodes.
|
||||||
|
|
||||||
|
.. deprecated:: 4.5
|
||||||
|
Use :class:`NodeIterator` instead, which provides more functionality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _query():
|
||||||
|
query_variables['first'] = self._graphql_page_length
|
||||||
|
try:
|
||||||
|
return edge_extractor(self.graphql_query(query_hash, query_variables, query_referer, rhx_gis))
|
||||||
|
except QueryReturnedBadRequestException:
|
||||||
|
new_page_length = int(self._graphql_page_length / 2)
|
||||||
|
if new_page_length >= 12:
|
||||||
|
self._graphql_page_length = new_page_length
|
||||||
|
self.error("HTTP Error 400 (Bad Request) on GraphQL Query. Retrying with shorter page length.",
|
||||||
|
repeat_at_end=False)
|
||||||
|
return _query()
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
if first_data:
|
||||||
|
data = first_data
|
||||||
|
else:
|
||||||
|
data = _query()
|
||||||
|
yield from (edge['node'] for edge in data['edges'])
|
||||||
|
while data['page_info']['has_next_page']:
|
||||||
|
query_variables['after'] = data['page_info']['end_cursor']
|
||||||
|
data = _query()
|
||||||
|
yield from (edge['node'] for edge in data['edges'])
|
||||||
|
|
||||||
|
def get_iphone_json(self, path: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""JSON request to ``i.instagram.com``.
|
||||||
|
|
||||||
|
:param path: URL, relative to ``i.instagram.com/``
|
||||||
|
:param params: GET parameters
|
||||||
|
:return: Decoded response dictionary
|
||||||
|
:raises QueryReturnedBadRequestException: When the server responds with a 400.
|
||||||
|
:raises QueryReturnedNotFoundException: When the server responds with a 404.
|
||||||
|
:raises ConnectionException: When query repeatedly failed.
|
||||||
|
|
||||||
|
.. versionadded:: 4.2.1"""
|
||||||
|
with copy_session(self._session, self.request_timeout) as tempsession:
|
||||||
|
tempsession.headers['User-Agent'] = 'Instagram 123.1.0.26.115 (iPhone12,1; iOS 13_3; en_US; en-US; ' \
|
||||||
|
'scale=2.00; 1656x3584; 190542906)'
|
||||||
|
for header in ['Host', 'Origin', 'X-Instagram-AJAX', 'X-Requested-With']:
|
||||||
|
tempsession.headers.pop(header, None)
|
||||||
|
return self.get_json(path, params, 'i.instagram.com', tempsession)
|
||||||
|
|
||||||
|
def write_raw(self, resp: Union[bytes, requests.Response], filename: str) -> None:
|
||||||
|
"""Write raw response data into a file.
|
||||||
|
|
||||||
|
.. versionadded:: 4.2.1"""
|
||||||
|
self.log(filename, end=' ', flush=True)
|
||||||
|
with open(filename + '.temp', 'wb') as file:
|
||||||
|
if isinstance(resp, requests.Response):
|
||||||
|
shutil.copyfileobj(resp.raw, file)
|
||||||
|
else:
|
||||||
|
file.write(resp)
|
||||||
|
os.rename(filename + '.temp', filename)
|
||||||
|
|
||||||
|
def get_raw(self, url: str, _attempt=1) -> requests.Response:
|
||||||
|
"""Downloads a file anonymously.
|
||||||
|
|
||||||
|
:raises QueryReturnedNotFoundException: When the server responds with a 404.
|
||||||
|
:raises QueryReturnedForbiddenException: When the server responds with a 403.
|
||||||
|
:raises ConnectionException: When download failed.
|
||||||
|
|
||||||
|
.. versionadded:: 4.2.1"""
|
||||||
|
with self.get_anonymous_session() as anonymous_session:
|
||||||
|
resp = anonymous_session.get(url, stream=True)
|
||||||
|
if resp.status_code == 200:
|
||||||
|
resp.raw.decode_content = True
|
||||||
|
return resp
|
||||||
|
else:
|
||||||
|
if resp.status_code == 403:
|
||||||
|
# suspected invalid URL signature
|
||||||
|
raise QueryReturnedForbiddenException("403 when accessing {}.".format(url))
|
||||||
|
if resp.status_code == 404:
|
||||||
|
# 404 not worth retrying.
|
||||||
|
raise QueryReturnedNotFoundException("404 when accessing {}.".format(url))
|
||||||
|
raise ConnectionException("HTTP error code {}.".format(resp.status_code))
|
||||||
|
|
||||||
|
def get_and_write_raw(self, url: str, filename: str) -> None:
|
||||||
|
"""Downloads and writes anonymously-requested raw data into a file.
|
||||||
|
|
||||||
|
:raises QueryReturnedNotFoundException: When the server responds with a 404.
|
||||||
|
:raises QueryReturnedForbiddenException: When the server responds with a 403.
|
||||||
|
:raises ConnectionException: When download repeatedly failed."""
|
||||||
|
self.write_raw(self.get_raw(url), filename)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def root_rhx_gis(self) -> Optional[str]:
|
||||||
|
"""rhx_gis string returned in the / query."""
|
||||||
|
if self.is_logged_in:
|
||||||
|
# At the moment, rhx_gis seems to be required for anonymous requests only. By returning None when logged
|
||||||
|
# in, we can save the root_rhx_gis lookup query.
|
||||||
|
return None
|
||||||
|
if self._root_rhx_gis is None:
|
||||||
|
self._root_rhx_gis = self.get_json('', {}).get('rhx_gis', '')
|
||||||
|
return self._root_rhx_gis or None
|
||||||
|
|
||||||
|
|
||||||
|
class RateController:
|
||||||
|
"""
|
||||||
|
Class providing request tracking and rate controlling to stay within rate limits.
|
||||||
|
|
||||||
|
It can be overridden to change Instaloader's behavior regarding rate limits, for example to raise a custom
|
||||||
|
exception when the rate limit is hit::
|
||||||
|
|
||||||
|
import instaloader
|
||||||
|
|
||||||
|
class MyRateController(instaloader.RateController):
|
||||||
|
def sleep(self, secs):
|
||||||
|
raise MyCustomException()
|
||||||
|
|
||||||
|
L = instaloader.Instaloader(rate_controller=lambda ctx: MyRateController(ctx))
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, context: InstaloaderContext):
|
||||||
|
self._context = context
|
||||||
|
self._graphql_query_timestamps = dict() # type: Dict[str, List[float]]
|
||||||
|
self._graphql_earliest_next_request_time = 0.0
|
||||||
|
|
||||||
|
def sleep(self, secs: float):
|
||||||
|
"""Wait given number of seconds."""
|
||||||
|
# Not static, to allow for the behavior of this method to depend on context-inherent properties, such as
|
||||||
|
# whether we are logged in.
|
||||||
|
# pylint:disable=no-self-use
|
||||||
|
time.sleep(secs)
|
||||||
|
|
||||||
|
def _dump_query_timestamps(self, current_time: float):
|
||||||
|
windows = [10, 11, 15, 20, 30, 60]
|
||||||
|
print("GraphQL requests:", file=sys.stderr)
|
||||||
|
for query_hash, times in self._graphql_query_timestamps.items():
|
||||||
|
print(" {}".format(query_hash), file=sys.stderr)
|
||||||
|
for window in windows:
|
||||||
|
reqs_in_sliding_window = sum(t > current_time - window * 60 for t in times)
|
||||||
|
print(" last {} minutes: {} requests".format(window, reqs_in_sliding_window), file=sys.stderr)
|
||||||
|
|
||||||
|
def count_per_sliding_window(self, query_type: str) -> int:
|
||||||
|
"""Return how many GraphQL requests can be done within the sliding window."""
|
||||||
|
# Not static, to allow for the count_per_sliding_window to depend on context-inherent properties, such as
|
||||||
|
# whether we are logged in.
|
||||||
|
# pylint:disable=no-self-use,unused-argument
|
||||||
|
return 200
|
||||||
|
|
||||||
|
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."""
|
||||||
|
sliding_window = 660
|
||||||
|
if query_type not in self._graphql_query_timestamps:
|
||||||
|
self._graphql_query_timestamps[query_type] = []
|
||||||
|
self._graphql_query_timestamps[query_type] = list(filter(lambda t: t > current_time - 60 * 60,
|
||||||
|
self._graphql_query_timestamps[query_type]))
|
||||||
|
reqs_in_sliding_window = list(filter(lambda t: t > current_time - sliding_window,
|
||||||
|
self._graphql_query_timestamps[query_type]))
|
||||||
|
count_per_sliding_window = self.count_per_sliding_window(query_type)
|
||||||
|
if len(reqs_in_sliding_window) < count_per_sliding_window and not untracked_queries:
|
||||||
|
return max(0.0, self._graphql_earliest_next_request_time - current_time)
|
||||||
|
next_request_time = min(reqs_in_sliding_window) + sliding_window + 6
|
||||||
|
if untracked_queries:
|
||||||
|
self._graphql_earliest_next_request_time = next_request_time
|
||||||
|
return max(next_request_time, self._graphql_earliest_next_request_time) - current_time
|
||||||
|
|
||||||
|
def wait_before_query(self, query_type: str) -> None:
|
||||||
|
"""This method is called before a query to Instagram. It calls :meth:`RateController.sleep` to wait
|
||||||
|
until the request can be made."""
|
||||||
|
waittime = self.query_waittime(query_type, time.monotonic(), False)
|
||||||
|
assert waittime >= 0
|
||||||
|
if waittime > 15:
|
||||||
|
self._context.log("\nToo many queries in the last time. Need to wait {} seconds, until {:%H:%M}."
|
||||||
|
.format(round(waittime), datetime.now() + timedelta(seconds=waittime)))
|
||||||
|
if waittime > 0:
|
||||||
|
self.sleep(waittime)
|
||||||
|
if query_type not in self._graphql_query_timestamps:
|
||||||
|
self._graphql_query_timestamps[query_type] = [time.monotonic()]
|
||||||
|
else:
|
||||||
|
self._graphql_query_timestamps[query_type].append(time.monotonic())
|
||||||
|
|
||||||
|
def handle_429(self, query_type: str) -> None:
|
||||||
|
"""This method is called to handle a 429 Too Many Requests response. It calls :meth:`RateController.sleep` to
|
||||||
|
wait until we can repeat the same request."""
|
||||||
|
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()
|
||||||
|
waittime = self.query_waittime(query_type, current_time, True)
|
||||||
|
assert waittime >= 0
|
||||||
|
if waittime > 15:
|
||||||
|
self._context.log("The request will be retried in {} seconds, at {:%H:%M}."
|
||||||
|
.format(round(waittime), datetime.now() + timedelta(seconds=waittime)))
|
||||||
|
self._dump_query_timestamps(current_time)
|
||||||
|
if waittime > 0:
|
||||||
|
self.sleep(waittime)
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from lzma import LZMAError
|
||||||
|
from typing import Any, Callable, Dict, Iterator, NamedTuple, Optional, Tuple, TypeVar
|
||||||
|
|
||||||
|
from .exceptions import InvalidArgumentException, QueryReturnedBadRequestException
|
||||||
|
from .instaloadercontext import InstaloaderContext
|
||||||
|
|
||||||
|
FrozenNodeIterator = NamedTuple('FrozenNodeIterator',
|
||||||
|
[('query_hash', str),
|
||||||
|
('query_variables', Dict),
|
||||||
|
('query_referer', Optional[str]),
|
||||||
|
('context_username', Optional[str]),
|
||||||
|
('total_index', int),
|
||||||
|
('best_before', Optional[float]),
|
||||||
|
('remaining_data', Optional[Dict])])
|
||||||
|
FrozenNodeIterator.__doc__ = \
|
||||||
|
"""A serializable representation of a :class:`NodeIterator` instance, saving its iteration state."""
|
||||||
|
FrozenNodeIterator.query_hash.__doc__ = """The GraphQL ``query_hash`` parameter."""
|
||||||
|
FrozenNodeIterator.query_variables.__doc__ = """The GraphQL ``query_variables`` parameter."""
|
||||||
|
FrozenNodeIterator.query_referer.__doc__ = """The HTTP referer used for the GraphQL query."""
|
||||||
|
FrozenNodeIterator.context_username.__doc__ = """The username who created the iterator, or ``None``."""
|
||||||
|
FrozenNodeIterator.total_index.__doc__ = """Number of items that have already been returned."""
|
||||||
|
FrozenNodeIterator.best_before.__doc__ = """Date when parts of the stored nodes might have expired."""
|
||||||
|
FrozenNodeIterator.remaining_data.__doc__ = \
|
||||||
|
"""The already-retrieved, yet-unprocessed ``edges`` and the ``page_info`` at time of freezing."""
|
||||||
|
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
|
class NodeIterator(Iterator[T]):
|
||||||
|
"""
|
||||||
|
Iterate the nodes within edges in a GraphQL pagination. Instances of this class are returned by many (but not all)
|
||||||
|
of Instaloader's :class:`Post`-returning functions (such as :meth:`Profile.get_posts` etc.).
|
||||||
|
|
||||||
|
What makes this iterator special is its ability to freeze/store its current state, e.g. to interrupt an iteration,
|
||||||
|
and later thaw/resume from where it left off.
|
||||||
|
|
||||||
|
You can freeze a NodeIterator with :meth:`NodeIterator.freeze`::
|
||||||
|
|
||||||
|
post_iterator = profile.get_posts()
|
||||||
|
try:
|
||||||
|
for post in post_iterator:
|
||||||
|
do_something_with(post)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
save("resume_information.json", post_iterator.freeze())
|
||||||
|
|
||||||
|
and later reuse it with :meth:`NodeIterator.thaw` on an equally-constructed NodeIterator::
|
||||||
|
|
||||||
|
post_iterator = profile.get_posts()
|
||||||
|
post_iterator.thaw(load("resume_information.json"))
|
||||||
|
|
||||||
|
A :class:`FrozenNodeIterator` can only be thawn with a matching NodeIterator, i.e. a NodeIterator instance that has
|
||||||
|
been constructed with the same parameters as the instance that is represented by the :class:`FrozenNodeIterator` in
|
||||||
|
question. This is to ensure that an iteration cannot be resumed in a wrong, unmatching loop. As a quick way to
|
||||||
|
distinguish iterators that are saved e.g. in files, there is the :attr:`NodeIterator.magic` string: Two
|
||||||
|
NodeIterators are matching if and only if they have the same magic.
|
||||||
|
|
||||||
|
See also :func:`resumable_iteration` for a high-level context manager that handles a resumable iteration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_graphql_page_length = 50
|
||||||
|
_shelf_life = timedelta(days=29)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
context: InstaloaderContext,
|
||||||
|
query_hash: str,
|
||||||
|
edge_extractor: Callable[[Dict[str, Any]], Dict[str, Any]],
|
||||||
|
node_wrapper: Callable[[Dict], T],
|
||||||
|
query_variables: Optional[Dict[str, Any]] = None,
|
||||||
|
query_referer: Optional[str] = None,
|
||||||
|
first_data: Optional[Dict[str, Any]] = None):
|
||||||
|
self._context = context
|
||||||
|
self._query_hash = query_hash
|
||||||
|
self._edge_extractor = edge_extractor
|
||||||
|
self._node_wrapper = node_wrapper
|
||||||
|
self._query_variables = query_variables if query_variables is not None else {}
|
||||||
|
self._query_referer = query_referer
|
||||||
|
self._data = first_data
|
||||||
|
self._page_index = 0
|
||||||
|
self._total_index = 0
|
||||||
|
self._best_before = (None if first_data is None else
|
||||||
|
datetime.now() + NodeIterator._shelf_life)
|
||||||
|
|
||||||
|
def _query(self, after: Optional[str] = None) -> Dict:
|
||||||
|
pagination_variables = {'first': NodeIterator._graphql_page_length} # type: Dict[str, Any]
|
||||||
|
if after is not None:
|
||||||
|
pagination_variables['after'] = after
|
||||||
|
try:
|
||||||
|
data = self._edge_extractor(
|
||||||
|
self._context.graphql_query(
|
||||||
|
self._query_hash, {**self._query_variables, **pagination_variables}, self._query_referer
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._best_before = datetime.now() + NodeIterator._shelf_life
|
||||||
|
return data
|
||||||
|
except QueryReturnedBadRequestException:
|
||||||
|
new_page_length = int(NodeIterator._graphql_page_length / 2)
|
||||||
|
if new_page_length >= 12:
|
||||||
|
NodeIterator._graphql_page_length = new_page_length
|
||||||
|
self._context.error("HTTP Error 400 (Bad Request) on GraphQL Query. Retrying with shorter page length.",
|
||||||
|
repeat_at_end=False)
|
||||||
|
return self._query(after)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self) -> T:
|
||||||
|
if self._data is None:
|
||||||
|
self._data = self._query()
|
||||||
|
if self._page_index < len(self._data['edges']):
|
||||||
|
node = self._data['edges'][self._page_index]['node']
|
||||||
|
page_index, total_index = self._page_index, self._total_index
|
||||||
|
try:
|
||||||
|
self._page_index += 1
|
||||||
|
self._total_index += 1
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self._page_index, self._total_index = page_index, total_index
|
||||||
|
raise
|
||||||
|
return self._node_wrapper(node)
|
||||||
|
if self._data['page_info']['has_next_page']:
|
||||||
|
query_response = self._query(self._data['page_info']['end_cursor'])
|
||||||
|
page_index, data = self._page_index, self._data
|
||||||
|
try:
|
||||||
|
self._page_index = 0
|
||||||
|
self._data = query_response
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self._page_index, self._data = page_index, data
|
||||||
|
raise
|
||||||
|
return self.__next__()
|
||||||
|
raise StopIteration()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def count(self) -> Optional[int]:
|
||||||
|
"""The ``count`` as returned by Instagram. This is not always the total count this iterator will yield."""
|
||||||
|
return self._data.get('count') if self._data is not None else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_index(self) -> int:
|
||||||
|
"""Number of items that have already been returned."""
|
||||||
|
return self._total_index
|
||||||
|
|
||||||
|
@property
|
||||||
|
def magic(self) -> str:
|
||||||
|
"""Magic string for easily identifying a matching iterator file for resuming (hash of some parameters)."""
|
||||||
|
if 'blake2b' not in hashlib.algorithms_available:
|
||||||
|
magic_hash = hashlib.new('sha224')
|
||||||
|
else:
|
||||||
|
# Use blake2b when possible, i.e. on Python >= 3.6.
|
||||||
|
magic_hash = hashlib.blake2b(digest_size=6) # type:ignore # pylint: disable=no-member
|
||||||
|
magic_hash.update(json.dumps(
|
||||||
|
[self._query_hash, self._query_variables, self._query_referer, self._context.username]
|
||||||
|
).encode())
|
||||||
|
return base64.urlsafe_b64encode(magic_hash.digest()).decode()
|
||||||
|
|
||||||
|
def freeze(self) -> FrozenNodeIterator:
|
||||||
|
"""Freeze the iterator for later resuming."""
|
||||||
|
remaining_data = None
|
||||||
|
if self._data is not None:
|
||||||
|
remaining_data = {**self._data,
|
||||||
|
'edges': (self._data['edges'][(max(self._page_index - 1, 0)):])}
|
||||||
|
return FrozenNodeIterator(
|
||||||
|
query_hash=self._query_hash,
|
||||||
|
query_variables=self._query_variables,
|
||||||
|
query_referer=self._query_referer,
|
||||||
|
context_username=self._context.username,
|
||||||
|
total_index=max(self.total_index - 1, 0),
|
||||||
|
best_before=self._best_before.timestamp() if self._best_before else None,
|
||||||
|
remaining_data=remaining_data,
|
||||||
|
)
|
||||||
|
|
||||||
|
def thaw(self, frozen: FrozenNodeIterator) -> None:
|
||||||
|
"""
|
||||||
|
Use this iterator for resuming from earlier iteration.
|
||||||
|
|
||||||
|
:raises InvalidArgumentException:
|
||||||
|
If
|
||||||
|
|
||||||
|
- the iterator on which this method is called has already been used, or
|
||||||
|
- the given :class:`FrozenNodeIterator` does not match, i.e. belongs to a different iteration.
|
||||||
|
"""
|
||||||
|
if self._total_index or self._page_index:
|
||||||
|
raise InvalidArgumentException("thaw() called on already-used iterator.")
|
||||||
|
if (self._query_hash != frozen.query_hash or
|
||||||
|
self._query_variables != frozen.query_variables or
|
||||||
|
self._query_referer != frozen.query_referer or
|
||||||
|
self._context.username != frozen.context_username):
|
||||||
|
raise InvalidArgumentException("Mismatching resume information.")
|
||||||
|
self._total_index = frozen.total_index
|
||||||
|
self._best_before = datetime.fromtimestamp(frozen.best_before) if frozen.best_before else None
|
||||||
|
self._data = frozen.remaining_data
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def resumable_iteration(context: InstaloaderContext,
|
||||||
|
iterator: Iterator,
|
||||||
|
load: Callable[[InstaloaderContext, str], Any],
|
||||||
|
save: Callable[[FrozenNodeIterator, str], None],
|
||||||
|
format_path: Callable[[str], str],
|
||||||
|
check_bbd: bool = True,
|
||||||
|
enabled: bool = True) -> Iterator[Tuple[bool, int]]:
|
||||||
|
"""
|
||||||
|
High-level context manager to handle a resumable iteration that can be interrupted with a KeyboardInterrupt.
|
||||||
|
|
||||||
|
It can be used as follows to automatically load a previously-saved state into the iterator, save the iterator's
|
||||||
|
state when interrupted, and delete the resume file upon completion::
|
||||||
|
|
||||||
|
post_iterator = profile.get_posts()
|
||||||
|
with resumable_iteration(
|
||||||
|
context=L.context,
|
||||||
|
iterator=post_iterator,
|
||||||
|
load=lambda _, path: FrozenNodeIterator(**json.load(open(path))),
|
||||||
|
save=lambda fni, path: json.dump(fni._asdict(), open(path, 'w')),
|
||||||
|
format_path=lambda magic: "resume_info_{}.json".format(magic)
|
||||||
|
) as (is_resuming, start_index):
|
||||||
|
for post in post_iterator:
|
||||||
|
do_something_with(post)
|
||||||
|
|
||||||
|
It yields a tuple (is_resuming, start_index).
|
||||||
|
|
||||||
|
When the passed iterator is not a :class:`NodeIterator`, it behaves as if ``resumable_iteration`` was not used,
|
||||||
|
just executing the inner body.
|
||||||
|
|
||||||
|
:param context: The :class:`InstaloaderContext`.
|
||||||
|
:param iterator: The fresh :class:`NodeIterator`.
|
||||||
|
:param load: Loads a FrozenNodeIterator from given path. The object is ignored if it has a different type.
|
||||||
|
:param save: Saves the given FrozenNodeIterator to the given path.
|
||||||
|
:param format_path: Returns the path to the resume file for the given magic.
|
||||||
|
:param check_bbd: Whether to check the best before date and reject an expired FrozenNodeIterator.
|
||||||
|
:param enabled: Set to False to disable all functionality and simply execute the inner body.
|
||||||
|
"""
|
||||||
|
if not enabled or not isinstance(iterator, NodeIterator):
|
||||||
|
yield False, 0
|
||||||
|
return
|
||||||
|
is_resuming = False
|
||||||
|
start_index = 0
|
||||||
|
resume_file_path = format_path(iterator.magic)
|
||||||
|
resume_file_exists = os.path.isfile(resume_file_path)
|
||||||
|
if resume_file_exists:
|
||||||
|
try:
|
||||||
|
fni = load(context, resume_file_path)
|
||||||
|
if not isinstance(fni, FrozenNodeIterator):
|
||||||
|
raise InvalidArgumentException("Invalid type.")
|
||||||
|
if check_bbd and fni.best_before and datetime.fromtimestamp(fni.best_before) < datetime.now():
|
||||||
|
raise InvalidArgumentException("\"Best before\" date exceeded.")
|
||||||
|
iterator.thaw(fni)
|
||||||
|
is_resuming = True
|
||||||
|
start_index = iterator.total_index
|
||||||
|
context.log("Resuming from {}.".format(resume_file_path))
|
||||||
|
except (InvalidArgumentException, LZMAError, json.decoder.JSONDecodeError) as exc:
|
||||||
|
context.error("Warning: Not resuming from {}: {}".format(resume_file_path, exc))
|
||||||
|
try:
|
||||||
|
yield is_resuming, start_index
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
os.makedirs(os.path.dirname(resume_file_path), exist_ok=True)
|
||||||
|
save(iterator.freeze(), resume_file_path)
|
||||||
|
context.log("\nSaved resume information to {}.".format(resume_file_path))
|
||||||
|
raise
|
||||||
|
if resume_file_exists:
|
||||||
|
os.unlink(resume_file_path)
|
||||||
|
context.log("Iteration complete, deleted resume information file {}.".format(resume_file_path))
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,35 +1,66 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
|
|
||||||
|
SRC = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
def get_version():
|
||||||
|
with open(os.path.join(SRC, 'instaloader/__init__.py')) as f:
|
||||||
|
for line in f:
|
||||||
|
m = re.match("__version__ = '(.*)'", line)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
raise SystemExit("Could not find version string.")
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info < (3, 5):
|
if sys.version_info < (3, 5):
|
||||||
sys.exit('Instaloader requires Python >= 3.5.')
|
sys.exit('Instaloader requires Python >= 3.5.')
|
||||||
|
|
||||||
|
requirements = ['requests>=2.4']
|
||||||
|
|
||||||
|
if platform.system() == 'Windows' and sys.version_info < (3, 6):
|
||||||
|
requirements.append('win_unicode_console')
|
||||||
|
|
||||||
|
keywords = (['instagram', 'instagram-scraper', 'instagram-client', 'instagram-feed', 'downloader', 'videos', 'photos',
|
||||||
|
'pictures', 'instagram-user-photos', 'instagram-photos', 'instagram-metadata', 'instagram-downloader',
|
||||||
|
'instagram-stories'])
|
||||||
|
|
||||||
|
# NOTE that many of the values defined in this file are duplicated on other places, such as the
|
||||||
|
# documentation.
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='instaloader',
|
name='instaloader',
|
||||||
version='2.2.2',
|
version=get_version(),
|
||||||
py_modules=['instaloader'],
|
packages=['instaloader'],
|
||||||
url='https://github.com/Thammus/instaloader',
|
url='https://instaloader.github.io/',
|
||||||
license='MIT',
|
license='MIT',
|
||||||
author='Alexander Graf, André Koch-Kramer',
|
author='Alexander Graf, André Koch-Kramer',
|
||||||
author_email='mail@agraf.me, koch-kramer@web.de',
|
author_email='mail@agraf.me, koch-kramer@web.de',
|
||||||
description='Download pictures (or videos) along with their captions and other metadata '
|
description='Download pictures (or videos) along with their captions and other metadata '
|
||||||
'from Instagram.',
|
'from Instagram.',
|
||||||
long_description=open('README.rst').read(),
|
long_description=open(os.path.join(SRC, 'README.rst')).read(),
|
||||||
install_requires=['requests>=2.4'],
|
install_requires=requirements,
|
||||||
python_requires='>=3.5',
|
python_requires='>=3.5',
|
||||||
entry_points={'console_scripts': ['instaloader=instaloader:main']},
|
entry_points={'console_scripts': ['instaloader=instaloader.__main__:main']},
|
||||||
zip_safe=True,
|
zip_safe=True,
|
||||||
keywords='instagram downloader',
|
keywords=keywords,
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 5 - Production/Stable',
|
'Development Status :: 5 - Production/Stable',
|
||||||
'Environment :: Console',
|
'Environment :: Console',
|
||||||
'Intended Audience :: End Users/Desktop',
|
'Intended Audience :: End Users/Desktop',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
'License :: OSI Approved :: MIT License',
|
'License :: OSI Approved :: MIT License',
|
||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
'Programming Language :: Python :: 3.5',
|
'Programming Language :: Python :: 3.5',
|
||||||
'Programming Language :: Python :: 3.6',
|
'Programming Language :: Python :: 3.6',
|
||||||
|
'Programming Language :: Python :: 3.7',
|
||||||
|
'Programming Language :: Python :: 3.8',
|
||||||
'Programming Language :: Python :: 3 :: Only',
|
'Programming Language :: Python :: 3 :: Only',
|
||||||
'Topic :: Internet',
|
'Topic :: Internet',
|
||||||
'Topic :: Multimedia :: Graphics'
|
'Topic :: Multimedia :: Graphics'
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
"""Unit Tests for Instaloader"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from itertools import islice
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import instaloader
|
||||||
|
|
||||||
|
PROFILE_WITH_HIGHLIGHTS = 325732271
|
||||||
|
PUBLIC_PROFILE = "selenagomez"
|
||||||
|
PUBLIC_PROFILE_ID = 460563723
|
||||||
|
PUBLIC_PROFILE_WITH_IGTV = "natgeo"
|
||||||
|
HASHTAG = "kitten"
|
||||||
|
LOCATION = "362629379"
|
||||||
|
OWN_USERNAME = "aandergr"
|
||||||
|
NORMAL_MAX_COUNT = 2
|
||||||
|
PAGING_MAX_COUNT = 15
|
||||||
|
PRIVATE_PROFILE = "aandergr"
|
||||||
|
PRIVATE_PROFILE_ID = 1706625676
|
||||||
|
EMPTY_PROFILE = "not_public"
|
||||||
|
EMPTY_PROFILE_ID = 1928659031
|
||||||
|
|
||||||
|
ratecontroller = None # type: Optional[instaloader.RateController]
|
||||||
|
|
||||||
|
|
||||||
|
class TestInstaloaderAnonymously(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.dir = tempfile.mkdtemp()
|
||||||
|
print("Testing in {}".format(self.dir))
|
||||||
|
os.chdir(self.dir)
|
||||||
|
self.L = instaloader.Instaloader(download_geotags=True,
|
||||||
|
download_comments=True,
|
||||||
|
save_metadata=True)
|
||||||
|
self.L.context.raise_all_errors = True
|
||||||
|
if ratecontroller is not None:
|
||||||
|
# pylint:disable=protected-access
|
||||||
|
ratecontroller._context = self.L.context
|
||||||
|
self.L.context._rate_controller = ratecontroller
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
# pylint:disable=global-statement,protected-access
|
||||||
|
global ratecontroller
|
||||||
|
ratecontroller = self.L.context._rate_controller
|
||||||
|
self.L.close()
|
||||||
|
os.chdir('/')
|
||||||
|
print("Removing {}".format(self.dir))
|
||||||
|
shutil.rmtree(self.dir)
|
||||||
|
|
||||||
|
def post_paging_test(self, iterator):
|
||||||
|
previous_post = None
|
||||||
|
for post in islice(iterator, PAGING_MAX_COUNT):
|
||||||
|
print(post)
|
||||||
|
if previous_post:
|
||||||
|
self.assertTrue(post.date_utc < previous_post.date_utc)
|
||||||
|
previous_post = post
|
||||||
|
|
||||||
|
@unittest.skip('')
|
||||||
|
def test_public_profile_download(self):
|
||||||
|
profiles = {self.L.check_profile_id(PUBLIC_PROFILE)}
|
||||||
|
self.L.download_profiles(profiles, profile_pic=False, fast_update=True, raise_errors=True)
|
||||||
|
self.L.download_profiles(profiles, profile_pic=False, fast_update=True, raise_errors=True)
|
||||||
|
|
||||||
|
def test_public_profile_paging(self):
|
||||||
|
self.post_paging_test(instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).get_posts())
|
||||||
|
|
||||||
|
def test_profile_pic_download(self):
|
||||||
|
self.L.download_profiles({self.L.check_profile_id(PUBLIC_PROFILE)}, posts=False, raise_errors=True)
|
||||||
|
|
||||||
|
def test_hashtag_download(self):
|
||||||
|
self.L.download_hashtag(HASHTAG, NORMAL_MAX_COUNT)
|
||||||
|
|
||||||
|
def test_hashtag_paging(self):
|
||||||
|
for count, post in enumerate(instaloader.Hashtag.from_name(self.L.context, HASHTAG).get_all_posts()):
|
||||||
|
print(post)
|
||||||
|
if count == PAGING_MAX_COUNT:
|
||||||
|
break
|
||||||
|
|
||||||
|
def test_get_id_by_username(self):
|
||||||
|
self.assertEqual(PUBLIC_PROFILE_ID,
|
||||||
|
instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).userid)
|
||||||
|
|
||||||
|
def test_get_username_by_id_private(self):
|
||||||
|
self.assertEqual(PRIVATE_PROFILE.lower(),
|
||||||
|
instaloader.Profile.from_id(self.L.context, PRIVATE_PROFILE_ID).username)
|
||||||
|
|
||||||
|
def test_get_username_by_id_public(self):
|
||||||
|
self.assertEqual(PUBLIC_PROFILE.lower(),
|
||||||
|
instaloader.Profile.from_id(self.L.context, PUBLIC_PROFILE_ID).username)
|
||||||
|
|
||||||
|
def test_get_username_by_id_empty(self):
|
||||||
|
self.assertEqual(EMPTY_PROFILE.lower(),
|
||||||
|
instaloader.Profile.from_id(self.L.context, EMPTY_PROFILE_ID).username)
|
||||||
|
|
||||||
|
def test_get_username_by_name_empty(self):
|
||||||
|
self.assertEqual(EMPTY_PROFILE_ID,
|
||||||
|
instaloader.Profile.from_username(self.L.context, EMPTY_PROFILE).userid)
|
||||||
|
|
||||||
|
def test_post_from_mediaid(self):
|
||||||
|
for post in instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).get_posts():
|
||||||
|
post2 = instaloader.Post.from_mediaid(self.L.context, post.mediaid)
|
||||||
|
self.assertEqual(post, post2)
|
||||||
|
break
|
||||||
|
|
||||||
|
def test_public_profile_tagged_paging(self):
|
||||||
|
for post in islice(instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).get_tagged_posts(),
|
||||||
|
PAGING_MAX_COUNT):
|
||||||
|
print(post)
|
||||||
|
|
||||||
|
def test_public_profile_igtv(self):
|
||||||
|
for post in islice(instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE_WITH_IGTV).get_igtv_posts(),
|
||||||
|
PAGING_MAX_COUNT):
|
||||||
|
print(post)
|
||||||
|
|
||||||
|
|
||||||
|
class TestInstaloaderLoggedIn(TestInstaloaderAnonymously):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.L.load_session_from_file(OWN_USERNAME)
|
||||||
|
|
||||||
|
@unittest.skip('')
|
||||||
|
def test_stories_download(self):
|
||||||
|
self.L.download_stories()
|
||||||
|
|
||||||
|
@unittest.skip('')
|
||||||
|
def test_private_profile_download(self):
|
||||||
|
self.L.download_profiles({self.L.check_profile_id(PRIVATE_PROFILE)}, stories=True, raise_errors=True)
|
||||||
|
|
||||||
|
def test_stories_paging(self):
|
||||||
|
for user_story in self.L.get_stories():
|
||||||
|
print("profile {}.".format(user_story.owner_username))
|
||||||
|
for item in user_story.get_items():
|
||||||
|
print(item)
|
||||||
|
|
||||||
|
def test_highlights_paging(self):
|
||||||
|
for user_highlight in self.L.get_highlights(PROFILE_WITH_HIGHLIGHTS):
|
||||||
|
print("Retrieving {} highlights \"{}\" from profile {}".format(user_highlight.itemcount,
|
||||||
|
user_highlight.title,
|
||||||
|
user_highlight.owner_username))
|
||||||
|
for item in user_highlight.get_items():
|
||||||
|
print(item)
|
||||||
|
|
||||||
|
def test_private_profile_paging(self):
|
||||||
|
self.post_paging_test(instaloader.Profile.from_username(self.L.context, PRIVATE_PROFILE).get_posts())
|
||||||
|
|
||||||
|
def test_feed_download(self):
|
||||||
|
self.L.download_feed_posts(NORMAL_MAX_COUNT)
|
||||||
|
|
||||||
|
def test_feed_paging(self):
|
||||||
|
for count, post in enumerate(self.L.get_feed_posts()):
|
||||||
|
print(post)
|
||||||
|
if count == PAGING_MAX_COUNT:
|
||||||
|
break
|
||||||
|
|
||||||
|
def test_saved_download(self):
|
||||||
|
self.L.download_saved_posts(NORMAL_MAX_COUNT)
|
||||||
|
|
||||||
|
def test_saved_paging(self):
|
||||||
|
for count, post in enumerate(instaloader.Profile.from_username(self.L.context, OWN_USERNAME).get_saved_posts()):
|
||||||
|
print(post)
|
||||||
|
if count == PAGING_MAX_COUNT:
|
||||||
|
break
|
||||||
|
|
||||||
|
def test_test_login(self):
|
||||||
|
self.assertEqual(OWN_USERNAME, self.L.test_login())
|
||||||
|
|
||||||
|
def test_followees_and_stories(self):
|
||||||
|
profile = instaloader.Profile.from_username(self.L.context, OWN_USERNAME)
|
||||||
|
followees = set(islice(profile.get_followees(), PAGING_MAX_COUNT))
|
||||||
|
self.L.download_profiles(followees, profile_pic=False, stories=True, posts=False, raise_errors=True)
|
||||||
|
|
||||||
|
def test_get_followees(self):
|
||||||
|
profile = instaloader.Profile.from_username(self.L.context, OWN_USERNAME)
|
||||||
|
for f in profile.get_followees():
|
||||||
|
print(f.username)
|
||||||
|
|
||||||
|
def test_get_followers(self):
|
||||||
|
profile = instaloader.Profile.from_username(self.L.context, OWN_USERNAME)
|
||||||
|
for f in profile.get_followers():
|
||||||
|
print(f.username)
|
||||||
|
|
||||||
|
def test_get_similar_accounts(self):
|
||||||
|
profile = instaloader.Profile.from_username(self.L.context, OWN_USERNAME)
|
||||||
|
for f in profile.get_similar_accounts():
|
||||||
|
print(f.username)
|
||||||
|
|
||||||
|
def test_get_likes(self):
|
||||||
|
for post in instaloader.Profile.from_username(self.L.context, OWN_USERNAME).get_posts():
|
||||||
|
for like in post.get_likes():
|
||||||
|
print(like.username)
|
||||||
|
break
|
||||||
|
|
||||||
|
def test_explore_paging(self):
|
||||||
|
for count, post in enumerate(self.L.get_explore_posts()):
|
||||||
|
print(post)
|
||||||
|
if count == PAGING_MAX_COUNT:
|
||||||
|
break
|
||||||
|
|
||||||
|
def test_location_download(self):
|
||||||
|
self.L.download_location(LOCATION, NORMAL_MAX_COUNT)
|
||||||
|
|
||||||
|
def test_location_paging(self):
|
||||||
|
for count, post in enumerate(self.L.get_location_posts(LOCATION)):
|
||||||
|
print(post)
|
||||||
|
if count == PAGING_MAX_COUNT:
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user