mirror of
https://github.com/instaloader/instaloader.git
synced 2026-10-17 22:22:17 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7efa2de6fe | ||
|
|
58c07c6393 | ||
|
|
bc859a1595 | ||
|
|
b5e50aa260 | ||
|
|
0cf00a6079 | ||
|
|
91e30cf33b | ||
|
|
da65e697a0 | ||
|
|
132e2fa668 | ||
|
|
e3aef9dc2b | ||
|
|
dd90abdb91 | ||
|
|
2e3c89dfe9 | ||
|
|
c4434125e8 | ||
|
|
75173094d6 | ||
|
|
559c7a9adf | ||
|
|
1e58dddb1f | ||
|
|
af898e2c31 | ||
|
|
8f680ae52d | ||
|
|
7efc78de12 | ||
|
|
543469296e | ||
|
|
6dd77e7b56 | ||
|
|
a4adeb0835 | ||
|
|
ad03b392db | ||
|
|
297a88f252 | ||
|
|
db2ae98928 | ||
|
|
b79a0a1e8b | ||
|
|
d3cd639789 | ||
|
|
618c23144e | ||
|
|
66165a9d5d | ||
|
|
27e42f661e | ||
|
|
78bbd697cd | ||
|
|
e4524e119a | ||
|
|
520b69d6ef | ||
|
|
25c4daa861 | ||
|
|
7d256c36c1 | ||
|
|
64908788d3 | ||
|
|
b7ef306374 | ||
|
|
813621026c | ||
|
|
44bd4d173b | ||
|
|
2393a1a7a4 | ||
|
|
5304e9794b | ||
|
|
ee3106814e | ||
|
|
4c6d6e50af | ||
|
|
af287c3fdd | ||
|
|
0bc9ab55dd | ||
|
|
e64394fd25 | ||
|
|
43cc15d1b3 | ||
|
|
a263ec85a6 | ||
|
|
487d2c366d | ||
|
|
e493fa9c2d | ||
|
|
2e482b063a | ||
|
|
0bc91b722e | ||
|
|
4264d40f19 | ||
|
|
b15a595083 | ||
|
|
71ae32c85e | ||
|
|
f5134ff95e | ||
|
|
347f01b7fc | ||
|
|
981eec9182 | ||
|
|
c54b61346e | ||
|
|
fe9c8e1604 | ||
|
|
524fb21b80 | ||
|
|
ed4add951f | ||
|
|
8e6b2d83d3 | ||
|
|
ba8faec6fe | ||
|
|
9a74d9e109 | ||
|
|
08f805482c | ||
|
|
308244b809 | ||
|
|
b0c42a2662 | ||
|
|
c5dacb5f6d | ||
|
|
3a314de191 | ||
|
|
3a95250265 | ||
|
|
2a3e5980ff | ||
|
|
8159229203 | ||
|
|
ae9a5efa85 | ||
|
|
e159065799 | ||
|
|
ed21752365 | ||
|
|
b0157d9bc1 | ||
|
|
754c3845d7 | ||
|
|
e3b8c38f8c | ||
|
|
7dac8ad1e5 | ||
|
|
b4cb9ea83a | ||
|
|
eb2accc088 | ||
|
|
66c257362d | ||
|
|
a0169b7094 | ||
|
|
0a9fccaf26 | ||
|
|
36d9f66e8e | ||
|
|
5e04063002 | ||
|
|
76e8154b58 | ||
|
|
4a969882b5 | ||
|
|
dc1eb2b193 | ||
|
|
2a95589a1d | ||
|
|
0c7398882b | ||
|
|
3b705fe5d1 | ||
|
|
3ac790d30f | ||
|
|
fbfcd5df18 | ||
|
|
b44a006344 | ||
|
|
253144203b | ||
|
|
7d0e46798e | ||
|
|
fd8245a216 | ||
|
|
3e335984f2 | ||
|
|
98791fb0aa | ||
|
|
65b12d650c | ||
|
|
1fb98b066a | ||
|
|
14f1c3cb82 | ||
|
|
7a36512093 | ||
|
|
f423aa1507 | ||
|
|
1875aede82 | ||
|
|
2cae933d66 | ||
|
|
5509ea7daf | ||
|
|
2b594909bb | ||
|
|
1852447825 | ||
|
|
baaa2c2998 | ||
|
|
47235bf2b8 | ||
|
|
6eea8e6fcd | ||
|
|
5166fe691a | ||
|
|
9fa01509df | ||
|
|
f0420e8a20 | ||
|
|
7cc13ee08e | ||
|
|
bf396f0e6c | ||
|
|
39566735f7 | ||
|
|
fff25d6f4a | ||
|
|
b52547ea1c | ||
|
|
08bf0fef29 | ||
|
|
5efa5b3637 | ||
|
|
82b46605a2 | ||
|
|
f80eebec7a | ||
|
|
5a92f264c8 | ||
|
|
acb65b5cca | ||
|
|
51a16a433e | ||
|
|
6145c5b383 | ||
|
|
8bc35cf3a1 | ||
|
|
a3dc75c04e | ||
|
|
e48251e75e | ||
|
|
3ebfe40729 | ||
|
|
ebdc5f1557 | ||
|
|
4c6a184390 | ||
|
|
ed9ca62a48 | ||
|
|
99c3960ad4 | ||
|
|
14124b617e | ||
|
|
b40a5aa243 | ||
|
|
cc8c9ff883 | ||
|
|
75d3ab69d1 | ||
|
|
79820bb442 | ||
|
|
f70a7eb4db | ||
|
|
2dca99b89e | ||
|
|
1074a74a59 | ||
|
|
aa59683bd1 | ||
|
|
6a3d763cf3 | ||
|
|
e2d42807a2 | ||
|
|
d07bd6a096 | ||
|
|
0462d5c34a | ||
|
|
1db182ec27 | ||
|
|
d09493e669 | ||
|
|
3cc29a4ceb | ||
|
|
ffb0aea5eb | ||
|
|
84bb60c155 | ||
|
|
7857909886 | ||
|
|
2206a7e5d7 | ||
|
|
f1b5e9cdf1 | ||
|
|
8debcbb7ab | ||
|
|
e97fd8a27c | ||
|
|
7f9d1618ad | ||
|
|
6a42906697 | ||
|
|
0570dd71d6 | ||
|
|
fb14c2699e | ||
|
|
8784ac7d9b | ||
|
|
4bb32e3c5a | ||
|
|
5e232fd4f5 | ||
|
|
da0dcb106c | ||
|
|
0c21da8c18 | ||
|
|
9b928b4679 | ||
|
|
8649a10113 | ||
|
|
d406de4f35 | ||
|
|
2d37e1dca2 | ||
|
|
37cbc5d72d | ||
|
|
862aa4e82c | ||
|
|
853e860363 | ||
|
|
90cfeaf471 | ||
|
|
5a24064012 | ||
|
|
644fab1f5e | ||
|
|
0704602e50 | ||
|
|
218d88fc6d | ||
|
|
385c6c8a35 | ||
|
|
c0e5d0475b | ||
|
|
fbd7df2a90 | ||
|
|
569808f7a4 | ||
|
|
b808843efe | ||
|
|
f66cdd3e07 | ||
|
|
83dc5f749d | ||
|
|
41de9e5a7c | ||
|
|
d938f3e2f5 | ||
|
|
374d9d0d81 | ||
|
|
3cbee1b318 | ||
|
|
dbc91cbeb1 | ||
|
|
5a42a7537f | ||
|
|
d8d268f861 | ||
|
|
463ada519e | ||
|
|
604b107586 | ||
|
|
37a93ee59a | ||
|
|
2d4f3b8537 | ||
|
|
8c752532e1 | ||
|
|
fdcf2e1367 | ||
|
|
ca78fee307 | ||
|
|
d6e5e31005 | ||
|
|
d864ce08ff | ||
|
|
555c86633c | ||
|
|
21365ec668 | ||
|
|
6343e9a69d | ||
|
|
06574eb428 | ||
|
|
815ba83d77 | ||
|
|
5bff9b5474 | ||
|
|
a23b832a69 | ||
|
|
5d18857695 | ||
|
|
d6fd4c560c | ||
|
|
470fc0d905 | ||
|
|
0300d8a4f1 | ||
|
|
f09bd51ed4 | ||
|
|
7a32272b55 | ||
|
|
9eb2339028 | ||
|
|
c45b536be9 | ||
|
|
2c116683c2 | ||
|
|
054fa47245 | ||
|
|
1f8ebe09b4 | ||
|
|
87dfdff335 | ||
|
|
631503d463 | ||
|
|
43c9d32b22 | ||
|
|
1ad9111753 | ||
|
|
327fcfd8e8 | ||
|
|
ae39ab9893 | ||
|
|
0372f7c8e8 | ||
|
|
aaeaaa0e22 | ||
|
|
35bd8357d1 | ||
|
|
9cdf679fc1 | ||
|
|
94d599db61 | ||
|
|
a0e3e8db0d | ||
|
|
26c9f71132 | ||
|
|
210719af56 | ||
|
|
8c1129e556 | ||
|
|
c200031be6 | ||
|
|
a0d2ec95b9 | ||
|
|
750dd3f47f | ||
|
|
839b77eef0 | ||
|
|
e346842f7b | ||
|
|
9b3f809a9a | ||
|
|
803440ac50 | ||
|
|
455a757159 | ||
|
|
07de73bde1 | ||
|
|
bf83d406d3 | ||
|
|
ac5d6e312a | ||
|
|
3bb4a5b4b1 | ||
|
|
9ffdf258a9 | ||
|
|
d530a7a37f | ||
|
|
d142fb70b0 | ||
|
|
8a26943215 | ||
|
|
5345470ebf | ||
|
|
a2715cbe03 | ||
|
|
fa19b95107 | ||
|
|
3837b642b0 | ||
|
|
e67b9e6c37 | ||
|
|
2bf175a111 | ||
|
|
95d8534aed | ||
|
|
b583cd07e1 | ||
|
|
52f258e89b | ||
|
|
e9888e72fd | ||
|
|
c72b2a8fc2 | ||
|
|
e2cec0a0bf | ||
|
|
08eaa7d474 | ||
|
|
f70355e803 | ||
|
|
bba7d5131c | ||
|
|
f6731566cd | ||
|
|
cd13211603 | ||
|
|
8635a576f0 | ||
|
|
8f594a3a30 | ||
|
|
0f11ef1bdd | ||
|
|
a4bb353421 | ||
|
|
dfdd744804 | ||
|
|
9fb112faa8 | ||
|
|
f85e49d521 | ||
|
|
6c4da39372 | ||
|
|
a2d756b177 | ||
|
|
e15d67c065 | ||
|
|
640066dd60 | ||
|
|
672b0b1840 | ||
|
|
828fa71ede | ||
|
|
4fd57f5598 | ||
|
|
c5b686a213 | ||
|
|
43c521afa9 | ||
|
|
3955540080 | ||
|
|
3fe6b1e139 | ||
|
|
a2ce906fd0 | ||
|
|
850cd39be4 | ||
|
|
227efa7b68 | ||
|
|
d7f7cad4a2 | ||
|
|
2f69ea62a1 | ||
|
|
0f258b6118 | ||
|
|
b31f279527 | ||
|
|
ed55ce140d | ||
|
|
dcc7ab89c1 | ||
|
|
610756d342 | ||
|
|
097bf7fecc | ||
|
|
3dcb0ff542 | ||
|
|
0be1129173 | ||
|
|
ace0e944ec | ||
|
|
3530cdc55c | ||
|
|
ed3fce45da | ||
|
|
a045168c79 | ||
|
|
4c02a186d3 | ||
|
|
e9648bc551 | ||
|
|
6d90c47d9a | ||
|
|
1c577716dd | ||
|
|
6824d7b75c | ||
|
|
6ad03d3575 | ||
|
|
03ac067e71 | ||
|
|
e70faf506d | ||
|
|
e11b88d44b | ||
|
|
cc2986cff3 | ||
|
|
c7372e36cc | ||
|
|
1c2e226630 | ||
|
|
74f7970b50 | ||
|
|
f4ecfea64c | ||
|
|
0586a1fec3 | ||
|
|
c53625028d | ||
|
|
4ad4584b0d | ||
|
|
886530d661 | ||
|
|
574cadbb82 | ||
|
|
6d3c73eec3 | ||
|
|
438669b954 | ||
|
|
a7a05c200c | ||
|
|
f53bdf9ef8 | ||
|
|
13612c606e | ||
|
|
b57bbe2af0 | ||
|
|
3a6ba6485e | ||
|
|
177a299ced | ||
|
|
4a1fe035af | ||
|
|
db86f81be7 | ||
|
|
020d412342 | ||
|
|
6f57197afb | ||
|
|
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 |
@@ -13,7 +13,7 @@ Steps to reproduce the behavior:
|
|||||||
(e.g. Instaloader command line)
|
(e.g. Instaloader command line)
|
||||||
|
|
||||||
**Expected behavior**
|
**Expected behavior**
|
||||||
A clear and concise description of what you expected to happen (if not obvious).
|
A clear and concise description of what you expected to happen (even if it seems obvious).
|
||||||
|
|
||||||
**Error messages and tracebacks**
|
**Error messages and tracebacks**
|
||||||
If applicable, add error messages and tracebacks to help explain your problem.
|
If applicable, add error messages and tracebacks to help explain your problem.
|
||||||
@@ -24,4 +24,9 @@ If applicable, add error messages and tracebacks to help explain your problem.
|
|||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
|
||||||
(please also see https://instaloader.github.io/contributing.html for how to report a bug)
|
<!--
|
||||||
|
Also see https://instaloader.github.io/contributing.html for how to report a bug.
|
||||||
|
|
||||||
|
Please understand that a bug issue that lacks the required information cannot be
|
||||||
|
processed.
|
||||||
|
-->
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -6,19 +6,26 @@ labels: "feature suggestion"
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Provide us a use case of the feature**
|
**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?
|
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**
|
**Describe the solution you'd like**
|
||||||
A clear and concise description of what you want to happen.
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
**Describe alternatives you've considered**
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
How do you achieve the here-described goal without the feature you are requesting?
|
||||||
|
|
||||||
**If the feature request is accepted, would you be willing to submit a PR?**
|
**If the feature request is accepted, would you be willing to submit a pull request?**
|
||||||
|
Yes / No
|
||||||
Yes / No _(Help can be provided if you need assistance submitting a PR)_
|
<!-- Help can be provided if you need assistance submitting a PR -->
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the feature request here.
|
Add any other context about the feature request here.
|
||||||
|
|
||||||
(please also see https://instaloader.github.io/contributing.html for how to suggest a feature)
|
<!--
|
||||||
|
Also see https://instaloader.github.io/contributing.html for how to suggest a feature.
|
||||||
|
|
||||||
|
Please understand that a feature suggestion issue that lacks the required information cannot be
|
||||||
|
processed.
|
||||||
|
-->
|
||||||
|
|||||||
@@ -6,3 +6,13 @@ labels: question
|
|||||||
---
|
---
|
||||||
|
|
||||||
Your question here...
|
Your question here...
|
||||||
|
|
||||||
|
<!--
|
||||||
|
People will be better able and more motivated to provide help if your question
|
||||||
|
|
||||||
|
- has a concise and meaningful title,
|
||||||
|
- is not covered by the documentation: https://instaloader.github.io/,
|
||||||
|
- is actually related to Instaloader (this is not a Python help forum),
|
||||||
|
- has not already been answered here or on other sites such as Stack Overflow: https://stackoverflow.com/questions/tagged/instaloader,
|
||||||
|
- is written in a polite and welcoming tone.
|
||||||
|
-->
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: pip
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
||||||
|
cooldown:
|
||||||
|
default-days: 7
|
||||||
|
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
||||||
|
cooldown:
|
||||||
|
default-days: 7
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
github: [aandergr]
|
||||||
|
custom: "https://instaloader.github.io/sponsors.html"
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Thanks for your willingness to contribute to Instaloader! Please briefly
|
||||||
|
describe:
|
||||||
|
|
||||||
|
- A motivation for this change, e.g.
|
||||||
|
- Fixes # .
|
||||||
|
- What problem does the pull request solve?
|
||||||
|
|
||||||
|
- The changes proposed in this pull request
|
||||||
|
- Keep the PR focused: avoid bundling independent fixes or refactorings.
|
||||||
|
|
||||||
|
- The completeness of this change
|
||||||
|
- Is the documentation updated (if appropriate)?
|
||||||
|
- How has it been tested?
|
||||||
|
- Do you consider it ready to be merged or is it a draft / proof of concept?
|
||||||
|
- Can we help you at some point?
|
||||||
|
|
||||||
|
- Further considerations
|
||||||
|
- Is it based on master (bugfix), upcoming/v4.X (new feature), or another pull request?
|
||||||
|
- Does it preserve backward compatibility and existing behavior?
|
||||||
|
|
||||||
|
-->
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: "Documentation"
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docs:
|
||||||
|
name: "Documentation"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: "Checkout Instaloader Repository"
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # needed for building docs
|
||||||
|
persist-credentials: false
|
||||||
|
- name: "Setup Python"
|
||||||
|
uses: actions/setup-python@v7
|
||||||
|
with:
|
||||||
|
python-version: 3.15
|
||||||
|
cache: 'pipenv'
|
||||||
|
- name: "Install Dependencies"
|
||||||
|
run: |
|
||||||
|
python -m pip install pipenv==2026.8.0
|
||||||
|
pipenv --python `python --version | grep -Eo '3\.[0-9]+'` sync --dev
|
||||||
|
- name: "Build Documentation"
|
||||||
|
run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run make -C docs html SPHINXOPTS="-W -n"
|
||||||
|
- name: "Deploy Documentation"
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||||
|
uses: JamesIves/github-pages-deploy-action@v4
|
||||||
|
with:
|
||||||
|
branch: master
|
||||||
|
repository-name: instaloader/instaloader.github.io
|
||||||
|
folder: docs/_build/html
|
||||||
|
token: ${{ secrets.PAGES_DEPLOYMENT_TOKEN }}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: PyLint and MyPy
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: PyLint and MyPy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
|
||||||
|
steps:
|
||||||
|
- name: Checkout Instaloader Repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v7
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
cache: 'pipenv'
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install pipenv==2026.8.0
|
||||||
|
pipenv --python `python --version | grep -Eo '3\.[0-9]+'` sync --dev
|
||||||
|
- name: PyLint
|
||||||
|
run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run pylint instaloader
|
||||||
|
- name: MyPy
|
||||||
|
run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run mypy -m instaloader
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
name: "Publish Packages"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
name: "Publish Package"
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: "Checkout Repository"
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: "Setup Python"
|
||||||
|
uses: actions/setup-python@v7
|
||||||
|
with:
|
||||||
|
python-version: 3.15
|
||||||
|
- name: "Get the tagged version"
|
||||||
|
id: get_version
|
||||||
|
env:
|
||||||
|
GITHUB_REF: ${{ github.ref }}
|
||||||
|
run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
|
||||||
|
shell: bash
|
||||||
|
- name: "Install Dependencies"
|
||||||
|
run: python -m pip install -U setuptools wheel build
|
||||||
|
- name: "Generate Python Package"
|
||||||
|
run: python -m build
|
||||||
|
- name: "Publish Distribution to PyPI"
|
||||||
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
with:
|
||||||
|
password: ${{ secrets.PYPI_PASSWORD }}
|
||||||
|
- name: "Publish Distribution to AUR"
|
||||||
|
run: deploy/arch/deploy.sh $VERSION_TAG
|
||||||
|
env:
|
||||||
|
VERSION_TAG: v${{ steps.get_version.outputs.VERSION }}
|
||||||
|
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||||
|
AUR_IV: ${{ secrets.AUR_IV }}
|
||||||
@@ -2,23 +2,33 @@ name: Mark stale issues and pull requests
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "15 16 * * *"
|
- cron: "55 7 * * *"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
|
|
||||||
|
name: Mark stale issues and pull requests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v1
|
- uses: actions/stale@v11
|
||||||
|
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: 21
|
||||||
|
days-before-close: -1
|
||||||
|
remove-stale-when-updated: false
|
||||||
|
- uses: actions/stale@v11
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
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-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-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-issue-label: 'stale'
|
||||||
stale-pr-label: 'stale'
|
stale-pr-label: 'stale'
|
||||||
exempt-issue-label: 'leave open'
|
exempt-issue-labels: 'leave open'
|
||||||
exempt-pr-label: 'leave open'
|
exempt-pr-labels: 'leave open'
|
||||||
days-before-stale: 90
|
days-before-stale: 189
|
||||||
days-before-close: 14
|
days-before-close: 14
|
||||||
operations-per-run: 7
|
|
||||||
|
|||||||
@@ -4,44 +4,38 @@ on: [push, pull_request]
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
name: Windows EXE build
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Instaloader repository
|
- name: Checkout Instaloader repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v7
|
||||||
- name: Setup Python
|
|
||||||
uses: actions/setup-python@v1
|
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
persist-credentials: false
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v7
|
||||||
|
with:
|
||||||
|
python-version: 3.15
|
||||||
|
cache: 'pipenv'
|
||||||
architecture: x64
|
architecture: x64
|
||||||
- name: Get the tagged version
|
- name: Get the tagged version
|
||||||
id: get_version
|
id: get_version
|
||||||
env:
|
env:
|
||||||
GITHUB_REF: ${{ github.ref }}
|
GITHUB_REF: ${{ github.ref }}
|
||||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/}
|
run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
|
||||||
shell: bash
|
shell: bash
|
||||||
- name: Run custom python script for EXE creation
|
- name: Run custom python script for EXE creation
|
||||||
env:
|
env:
|
||||||
VERSION_TAG: v${{ steps.get_version.outputs.VERSION }}
|
VERSION_TAG: v${{ steps.get_version.outputs.VERSION }}
|
||||||
run: python deploy/windows/create_exe.py
|
run: python deploy/windows/create_exe.py
|
||||||
- name: Create draft release
|
- name: Create draft release and upload EXE as release asset
|
||||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: actions/create-release@v1
|
uses: softprops/action-gh-release@v3
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
tag_name: v${{ steps.get_version.outputs.VERSION }}
|
tag_name: v${{ steps.get_version.outputs.VERSION }}
|
||||||
release_name: Version ${{ steps.get_version.outputs.VERSION }}
|
name: Version ${{ steps.get_version.outputs.VERSION }}
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: contains(steps.get_version.outputs.VERSION, 'a') || contains(steps.get_version.outputs.VERSION, 'b') || contains(steps.get_version.outputs.VERSION, 'c')
|
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
|
files: ./instaloader-v${{ steps.get_version.outputs.VERSION }}-windows-standalone.zip
|
||||||
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
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
-36
@@ -1,36 +0,0 @@
|
|||||||
dist: xenial
|
|
||||||
language: python
|
|
||||||
python:
|
|
||||||
- "3.5"
|
|
||||||
- "3.6"
|
|
||||||
- "3.7"
|
|
||||||
install:
|
|
||||||
- pipenv sync --dev
|
|
||||||
script:
|
|
||||||
- pylint instaloader
|
|
||||||
- mypy -m instaloader
|
|
||||||
- make -C docs html SPHINXOPTS="-W -n"
|
|
||||||
addons:
|
|
||||||
ssh_known_hosts: aur.archlinux.org
|
|
||||||
deploy:
|
|
||||||
- provider: pypi
|
|
||||||
user: aandergr
|
|
||||||
on:
|
|
||||||
tags: true
|
|
||||||
python: 3.6
|
|
||||||
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=
|
|
||||||
- provider: pages
|
|
||||||
skip-cleanup: true
|
|
||||||
github-token: $GITHUB_TOKEN
|
|
||||||
local-dir: docs/_build/html
|
|
||||||
repo: instaloader/instaloader.github.io
|
|
||||||
target-branch: master
|
|
||||||
on:
|
|
||||||
tags: true
|
|
||||||
python: 3.6
|
|
||||||
- provider: script
|
|
||||||
script: deploy/arch/deploy.sh $TRAVIS_TAG
|
|
||||||
on:
|
|
||||||
tags: true
|
|
||||||
python: 3.6
|
|
||||||
@@ -6,3 +6,5 @@ Instaloader is written by
|
|||||||
- Alexander Graf (@aandergr)
|
- Alexander Graf (@aandergr)
|
||||||
- André Koch-Kramer (@Thammus)
|
- André Koch-Kramer (@Thammus)
|
||||||
- Lars Lindqvist (@e5150)
|
- Lars Lindqvist (@e5150)
|
||||||
|
- Eduardo M Kalinowksi (@ekalin)
|
||||||
|
- ... and many more, see https://github.com/instaloader/instaloader/graphs/contributors
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement by opening an
|
||||||
|
issue or contacting one or more of the project maintainers.
|
||||||
|
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
|
||||||
|
at [https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
|
||||||
|
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||||
|
[FAQ]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2016-2019 Alexander Graf and André Koch-Kramer.
|
Copyright (c) 2016-2020 Alexander Graf and André Koch-Kramer.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
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
|
||||||
|
|||||||
@@ -8,9 +8,16 @@ pylint = "*"
|
|||||||
mypy = "*"
|
mypy = "*"
|
||||||
sphinx = "*"
|
sphinx = "*"
|
||||||
pyinstaller = "*"
|
pyinstaller = "*"
|
||||||
pefile = "*"
|
|
||||||
pywin32-ctypes = "*"
|
|
||||||
psutil = "*"
|
psutil = "*"
|
||||||
|
types-requests = "*"
|
||||||
|
sphinx-autodoc-typehints = "*"
|
||||||
|
sphinxcontrib-jquery = "*"
|
||||||
|
tomli = "*" # Needed for Pylint for "python_version < '3.11'"
|
||||||
|
dill = "*" # Needed for Pylint for "python_version < '3.11'"
|
||||||
|
pywin32-ctypes = "*" # Needed for pyinstaller on Windows
|
||||||
|
pefile = "*" # Needed for pyinstaller on Windows
|
||||||
|
pip-licenses = "*"
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
requests = "*"
|
requests = "*"
|
||||||
|
browser-cookie3 = "*"
|
||||||
|
|||||||
Generated
+1284
-238
File diff suppressed because it is too large
Load Diff
+71
-31
@@ -2,11 +2,7 @@
|
|||||||
|
|
||||||
.. badges-start
|
.. badges-start
|
||||||
|
|
||||||
|travis| |pypi| |pyversion| |license| |aur| |contributors| |downloads| |saythanks|
|
|pypi| |pyversion| |license| |aur| |contributors| |downloads|
|
||||||
|
|
||||||
.. |travis| image:: https://img.shields.io/travis/instaloader/instaloader/master.svg
|
|
||||||
:alt: Travis-CI Build Status
|
|
||||||
:target: https://travis-ci.org/instaloader/instaloader
|
|
||||||
|
|
||||||
.. |pypi| image:: https://img.shields.io/pypi/v/instaloader.svg
|
.. |pypi| image:: https://img.shields.io/pypi/v/instaloader.svg
|
||||||
:alt: Instaloader PyPI Project Page
|
:alt: Instaloader PyPI Project Page
|
||||||
@@ -23,16 +19,13 @@
|
|||||||
:alt: Contributor Count
|
:alt: Contributor Count
|
||||||
:target: https://github.com/instaloader/instaloader/graphs/contributors
|
:target: https://github.com/instaloader/instaloader/graphs/contributors
|
||||||
|
|
||||||
.. |saythanks| image:: https://img.shields.io/badge/Say%20Thanks-!-ff7efd.svg
|
|
||||||
:alt: Say Thanks!
|
|
||||||
:target: https://saythanks.io/to/aandergr
|
|
||||||
|
|
||||||
.. |aur| image:: https://img.shields.io/aur/version/instaloader.svg
|
.. |aur| image:: https://img.shields.io/aur/version/instaloader.svg
|
||||||
:alt: Arch User Repository Package
|
:alt: Arch User Repository Package
|
||||||
:target: https://aur.archlinux.org/packages/instaloader/
|
:target: https://aur.archlinux.org/packages/instaloader/
|
||||||
|
|
||||||
.. |downloads| image:: https://img.shields.io/pypi/dm/instaloader.svg
|
.. |downloads| image:: https://pepy.tech/badge/instaloader/month
|
||||||
:alt: PyPI Download Count
|
:alt: PyPI Download Count
|
||||||
|
:target: https://pepy.tech/project/instaloader
|
||||||
|
|
||||||
.. badges-end
|
.. badges-end
|
||||||
|
|
||||||
@@ -53,11 +46,14 @@
|
|||||||
directory accordingly,
|
directory accordingly,
|
||||||
|
|
||||||
- allows **fine-grained customization** of filters and where to store
|
- allows **fine-grained customization** of filters and where to store
|
||||||
downloaded media.
|
downloaded media,
|
||||||
|
|
||||||
|
- automatically **resumes previously-interrupted** download iterations.
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
instaloader [--comments] [--geotags] [--stories] [--highlights] [--tagged]
|
instaloader [--comments] [--geotags]
|
||||||
|
[--stories] [--highlights] [--tagged] [--reels] [--igtv]
|
||||||
[--login YOUR-USERNAME] [--fast-update]
|
[--login YOUR-USERNAME] [--fast-update]
|
||||||
profile | "#hashtag" | :stories | :feed | :saved
|
profile | "#hashtag" | :stories | :feed | :saved
|
||||||
|
|
||||||
@@ -84,7 +80,19 @@ To later **update your local copy** of that profiles, you may run
|
|||||||
instaloader --fast-update profile [profile ...]
|
instaloader --fast-update profile [profile ...]
|
||||||
|
|
||||||
If ``--fast-update`` is given, Instaloader stops when arriving at the
|
If ``--fast-update`` is given, Instaloader stops when arriving at the
|
||||||
first already-downloaded picture. When updating profiles, Instaloader
|
first already-downloaded picture.
|
||||||
|
|
||||||
|
Alternatively, you can use ``--latest-stamps`` to have Instaloader store
|
||||||
|
the time each profile was last downloaded and only download newer media:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
instaloader --latest-stamps -- profile [profile ...]
|
||||||
|
|
||||||
|
With this option it's possible to move or delete downloaded media and still keep
|
||||||
|
the archive updated.
|
||||||
|
|
||||||
|
When updating profiles, Instaloader
|
||||||
automatically **detects profile name changes** and renames the target directory
|
automatically **detects profile name changes** and renames the target directory
|
||||||
accordingly.
|
accordingly.
|
||||||
|
|
||||||
@@ -102,6 +110,56 @@ already have a valid session cookie file.
|
|||||||
|
|
||||||
`Instaloader Documentation <https://instaloader.github.io/basic-usage.html>`__
|
`Instaloader Documentation <https://instaloader.github.io/basic-usage.html>`__
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Supporters
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. current-sponsors-start
|
||||||
|
|
||||||
|
| Instaloader is proudly sponsored by
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
* - |socialapis|
|
||||||
|
* - `SocialAPIs <https://socialapis.io/?utm_source=github&utm_medium=sponsor&utm_campaign=instaloader>`__
|
||||||
|
|
||||||
|
|
|
||||||
|
|
||||||
|
.. list-table::
|
||||||
|
:align: center
|
||||||
|
|
||||||
|
* - `@rocketapi-io <https://github.com/rocketapi-io>`__
|
||||||
|
|
||||||
|
|
|
||||||
|
|
||||||
|
See `Alex' GitHub Sponsors <https://github.com/sponsors/aandergr>`__ page for
|
||||||
|
how you can sponsor the development of Instaloader!
|
||||||
|
|
||||||
|
.. |socialapis| image:: ./.sponsor-logos/socialapis.png
|
||||||
|
:alt: SocialAPIs
|
||||||
|
:target: https://socialapis.io/?utm_source=github&utm_medium=sponsor&utm_campaign=instaloader
|
||||||
|
:height: 96px
|
||||||
|
|
||||||
|
.. current-sponsors-end
|
||||||
|
|
||||||
|
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 GitHub Sponsors page, a PayPal.Me link and a Bitcoin address.
|
||||||
|
| GitHub Sponsors: `Sponsor @aandergr on GitHub Sponsors <https://github.com/sponsors/aandergr>`__
|
||||||
|
| PayPal: `PayPal.me/aandergr <https://www.paypal.me/aandergr>`__
|
||||||
|
| BTC: 1Nst4LoadeYzrKjJ1DX9CpbLXBYE9RKLwY
|
||||||
|
|
||||||
Disclaimer
|
Disclaimer
|
||||||
----------
|
----------
|
||||||
@@ -114,21 +172,3 @@ subsidiaries. This is an independent and unofficial project. Use at your own ris
|
|||||||
Instaloader is licensed under an MIT license. Refer to ``LICENSE`` file for more information.
|
Instaloader is licensed under an MIT license. Refer to ``LICENSE`` file for more information.
|
||||||
|
|
||||||
.. disclaimer-end
|
.. 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
|
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ pkgbase = instaloader
|
|||||||
url = https://instaloader.github.io/
|
url = https://instaloader.github.io/
|
||||||
arch = any
|
arch = any
|
||||||
license = MIT
|
license = MIT
|
||||||
|
makedepends = python-build
|
||||||
|
makedepends = python-installer
|
||||||
makedepends = python-setuptools
|
makedepends = python-setuptools
|
||||||
depends = python>=3.5
|
makedepends = python-wheel
|
||||||
depends = python-requests>=2.4
|
depends = python
|
||||||
|
depends = python-requests
|
||||||
|
optdepends = python-browser-cookie3: Import session cookies from browser
|
||||||
options = !emptydirs
|
options = !emptydirs
|
||||||
source = instaloader-{{version}}.tar.gz::https://codeload.github.com/instaloader/instaloader/tar.gz/v{{version}}
|
source = instaloader-{{version}}.tar.gz::https://codeload.github.com/instaloader/instaloader/tar.gz/v{{version}}
|
||||||
sha512sums = {{hash}}
|
sha512sums = {{hash}}
|
||||||
|
|||||||
@@ -8,20 +8,30 @@ arch=('any')
|
|||||||
url="https://instaloader.github.io/"
|
url="https://instaloader.github.io/"
|
||||||
license=('MIT')
|
license=('MIT')
|
||||||
groups=()
|
groups=()
|
||||||
depends=('python>=3.5' 'python-requests>=2.4')
|
depends=(
|
||||||
makedepends=('python-setuptools')
|
python
|
||||||
|
python-requests
|
||||||
|
)
|
||||||
|
makedepends=(
|
||||||
|
python-build
|
||||||
|
python-installer
|
||||||
|
python-setuptools
|
||||||
|
python-wheel
|
||||||
|
)
|
||||||
|
optdepends=(
|
||||||
|
'python-browser-cookie3: Import session cookies from browser'
|
||||||
|
)
|
||||||
options=('!emptydirs')
|
options=('!emptydirs')
|
||||||
source=($pkgname-$pkgver.tar.gz::https://codeload.github.com/instaloader/instaloader/tar.gz/v$pkgver)
|
source=($pkgname-$pkgver.tar.gz::https://codeload.github.com/instaloader/instaloader/tar.gz/v$pkgver)
|
||||||
sha512sums=('{{hash}}')
|
sha512sums=('{{hash}}')
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
cd "$srcdir/$pkgname-$pkgver"
|
cd "$srcdir/$pkgname-$pkgver"
|
||||||
python setup.py clean --all
|
python -m build --wheel --no-isolation
|
||||||
python setup.py build
|
|
||||||
}
|
}
|
||||||
|
|
||||||
package() {
|
package() {
|
||||||
cd "$srcdir/$pkgname-$pkgver"
|
cd "$srcdir/$pkgname-$pkgver"
|
||||||
install -Dm644 LICENSE "$pkgdir"/usr/share/licenses/$pkgname/LICENSE
|
install -Dm644 LICENSE "$pkgdir"/usr/share/licenses/$pkgname/LICENSE
|
||||||
python setup.py install --root "$pkgdir/" --skip-build
|
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ echo $VERSION | grep -qe "[abc]" && exit 0
|
|||||||
cd $(dirname $0)
|
cd $(dirname $0)
|
||||||
|
|
||||||
# decrypt and add ssh key
|
# 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
|
openssl aes-256-cbc -K $AUR_KEY -iv $AUR_IV -in id_rsa_AUR.enc -out /tmp/AUR_openssh -d
|
||||||
eval "$(ssh-agent -s)"
|
eval "$(ssh-agent -s)"
|
||||||
chmod 600 /tmp/AUR_openssh
|
chmod 600 /tmp/AUR_openssh
|
||||||
ssh-add /tmp/AUR_openssh
|
ssh-add /tmp/AUR_openssh
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
ssh-keyscan -t rsa aur.archlinux.org >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
# clone and modify AUR repo
|
# clone and modify AUR repo
|
||||||
git clone --depth 1 ssh://aur@aur.archlinux.org/instaloader.git
|
git clone --depth 1 ssh://aur@aur.archlinux.org/instaloader.git
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
instaloader.exe is a standalone executable built with PyInstaller. Besides Instaloader itself
|
||||||
|
(see ../LICENSE.txt), it contains the following third-party software:
|
||||||
|
|
||||||
|
- Python, its standard library and the libraries it is built with, see Python.txt and
|
||||||
|
Python-incorporated-software.txt.
|
||||||
|
- The Python packages listed in third-party.txt, along with their license texts.
|
||||||
|
|
||||||
|
browser-cookie3 is licensed under the GNU Lesser General Public License version 3, whose text is
|
||||||
|
included in third-party.txt. It refers to the GNU General Public License version 3, see GPL-3.0.txt.
|
||||||
|
certifi is licensed under the Mozilla Public License 2.0, whose text is included in third-party.txt.
|
||||||
|
Their source code, in the versions listed in third-party.txt, is available at
|
||||||
|
https://pypi.org/project/browser-cookie3/#history and https://pypi.org/project/certifi/#history.
|
||||||
|
|
||||||
|
You may build instaloader.exe with modified versions of these packages: the source code of
|
||||||
|
Instaloader, including the script used to build instaloader.exe (deploy/windows/create_exe.py),
|
||||||
|
is published along with each release at https://github.com/instaloader/instaloader/releases.
|
||||||
@@ -1,40 +1,28 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
# copy the required files into repo root
|
# copy the required files into repo root
|
||||||
shutil.copy('docs/favicon.ico', '.')
|
shutil.copy('docs/favicon.ico', '.')
|
||||||
shutil.copy('deploy/windows/instaloader.spec', '.')
|
shutil.copy('deploy/windows/instaloader.spec', '.')
|
||||||
shutil.unpack_archive('deploy/windows/ps', '.', 'xztar')
|
|
||||||
|
|
||||||
code = """
|
code = """
|
||||||
|
import contextlib
|
||||||
import psutil
|
import psutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
def __main():
|
def __main():
|
||||||
grandparentpid = psutil.Process(os.getppid()).ppid()
|
with contextlib.suppress(AttributeError, psutil.Error):
|
||||||
grandparentpidsearchstring = ' ' + str(grandparentpid) + ' '
|
if psutil.Process().parent().parent().name() == "explorer.exe":
|
||||||
if hasattr(sys, "_MEIPASS"):
|
subprocess.Popen("powershell -NoExit -Command \\\"& '{0}'\\\"".format(sys.argv[0]))
|
||||||
ps = os.path.join(sys._MEIPASS, 'tasklist.exe')
|
return
|
||||||
else:
|
main()
|
||||||
ps = 'tasklist'
|
|
||||||
popen = subprocess.Popen(ps, stdout=subprocess.PIPE, universal_newlines=True)
|
|
||||||
for examine in iter(popen.stdout.readline, ""):
|
|
||||||
if grandparentpidsearchstring in examine:
|
|
||||||
pname = examine
|
|
||||||
break
|
|
||||||
popen.stdout.close()
|
|
||||||
return_code = popen.wait()
|
|
||||||
if return_code:
|
|
||||||
raise subprocess.CalledProcessError(return_code, ps)
|
|
||||||
if pname[0:12] == 'explorer.exe':
|
|
||||||
subprocess.Popen("cmd /K \\\"{0}\\\"".format(os.path.splitext(os.path.basename(sys.argv[0]))[0]))
|
|
||||||
else:
|
|
||||||
main()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
__main()
|
__main()
|
||||||
@@ -58,10 +46,18 @@ with open('instaloader/__main__.py', 'r') as f:
|
|||||||
with open('__main__.py', 'w+') as f:
|
with open('__main__.py', 'w+') as f:
|
||||||
f.writelines(lines)
|
f.writelines(lines)
|
||||||
|
|
||||||
# install dependencies and invoke PyInstaller
|
# packages bundled into the executable, whose licenses have to be shipped along with it
|
||||||
commands = ["pip install pipenv==2018.11.26",
|
with open('Pipfile.lock') as f:
|
||||||
"pipenv sync --dev",
|
locked_packages = json.load(f)['default']
|
||||||
"pipenv run pyinstaller --log-level=DEBUG instaloader.spec"]
|
bundled_packages = ' '.join(list(locked_packages) + ['psutil', 'pyinstaller'])
|
||||||
|
os.makedirs('dist/licenses', exist_ok=True)
|
||||||
|
|
||||||
|
# install dependencies, invoke PyInstaller and collect licenses
|
||||||
|
commands = ["pip install pipenv==2026.8.0",
|
||||||
|
f"pipenv --python {sys.version_info.major}.{sys.version_info.minor} sync --dev",
|
||||||
|
"pipenv run pyinstaller --log-level=DEBUG instaloader.spec",
|
||||||
|
f"pipenv run pip-licenses --packages {bundled_packages} --with-license-file "
|
||||||
|
"--with-notice-file --no-license-path --format=plain-vertical --output-file dist/licenses/third-party.txt"]
|
||||||
|
|
||||||
for command in commands:
|
for command in commands:
|
||||||
print()
|
print()
|
||||||
@@ -82,5 +78,13 @@ with open('dist/instaloader.exe', 'rb') as f:
|
|||||||
with open('dist/instaloader.exe.md5', 'w+') as f:
|
with open('dist/instaloader.exe.md5', 'w+') as f:
|
||||||
f.write('{} *instaloader.exe\n'.format(hash_md5.hexdigest()))
|
f.write('{} *instaloader.exe\n'.format(hash_md5.hexdigest()))
|
||||||
|
|
||||||
|
# include the remaining license texts
|
||||||
|
shutil.copy('LICENSE', 'dist/LICENSE.txt')
|
||||||
|
shutil.copy('deploy/windows/NOTICE.txt', 'dist/licenses/NOTICE.txt')
|
||||||
|
shutil.copy('deploy/windows/GPL-3.0.txt', 'dist/licenses/GPL-3.0.txt')
|
||||||
|
shutil.copy(os.path.join(sys.base_prefix, 'LICENSE.txt'), 'dist/licenses/Python.txt')
|
||||||
|
urllib.request.urlretrieve('https://raw.githubusercontent.com/python/cpython/v{}/Doc/license.rst'
|
||||||
|
.format(platform.python_version()), 'dist/licenses/Python-incorporated-software.txt')
|
||||||
|
|
||||||
# Create ZIP file
|
# Create ZIP file
|
||||||
shutil.make_archive('instaloader-{}-windows-standalone'.format(os.getenv('VERSION_TAG')), 'zip', 'dist')
|
shutil.make_archive('instaloader-{}-windows-standalone'.format(os.getenv('VERSION_TAG')), 'zip', 'dist')
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ block_cipher = None
|
|||||||
|
|
||||||
a = Analysis(['__main__.py'],
|
a = Analysis(['__main__.py'],
|
||||||
binaries=[],
|
binaries=[],
|
||||||
datas=[('tasklist.exe', '.'), ('framedyn.dll', '.')],
|
datas=[],
|
||||||
hiddenimports=[],
|
hiddenimports=[],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
runtime_hooks=[],
|
runtime_hooks=[],
|
||||||
excludes=[],
|
excludes=['setuptools', 'pkg_resources', '_distutils_hack', 'packaging'],
|
||||||
win_no_prefer_redirects=False,
|
win_no_prefer_redirects=False,
|
||||||
win_private_assemblies=False,
|
win_private_assemblies=False,
|
||||||
cipher=block_cipher)
|
cipher=block_cipher)
|
||||||
|
|||||||
Binary file not shown.
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../.sponsor-logos
|
||||||
+4
-3
@@ -7,9 +7,10 @@ https://instaloader.github.io/
|
|||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
The documentation is created with [Sphinx](http://www.sphinx-doc.org/). To build it, use
|
The documentation is created with [Sphinx](http://www.sphinx-doc.org/). To build it, use (in the top-level directory
|
||||||
|
of the Instaloader source tree):
|
||||||
|
|
||||||
```
|
```
|
||||||
pip3 install -r requirements.txt
|
pipenv sync --dev
|
||||||
make html
|
make -C docs html
|
||||||
```
|
```
|
||||||
|
|||||||
-7
File diff suppressed because one or more lines are too long
Vendored
-7
File diff suppressed because one or more lines are too long
Vendored
-75
@@ -1,75 +0,0 @@
|
|||||||
@import url(bootstrap-4.1.3.min.css);
|
|
||||||
a {
|
|
||||||
color: #008d06; }
|
|
||||||
a:hover, a.current, a.active {
|
|
||||||
color: #f48400; }
|
|
||||||
a.current, a.active {
|
|
||||||
font-weight: 500; }
|
|
||||||
|
|
||||||
.doc-sidebar a:hover, .modal-body a:hover {
|
|
||||||
text-decoration: none; }
|
|
||||||
|
|
||||||
code {
|
|
||||||
color: #212529; }
|
|
||||||
|
|
||||||
.highlight {
|
|
||||||
border-radius: 0.3em; }
|
|
||||||
.highlight pre {
|
|
||||||
padding: 0.7em;
|
|
||||||
color: #fff; }
|
|
||||||
.highlight .c1 {
|
|
||||||
color: #008d06; }
|
|
||||||
.highlight .k, .highlight .kn, .highlight .ow {
|
|
||||||
color: #f48400;
|
|
||||||
font-weight: bold; }
|
|
||||||
.highlight .nb {
|
|
||||||
color: #f48400; }
|
|
||||||
.highlight .s1, .highlight .s2, .highlight .mi {
|
|
||||||
color: #ff7efd; }
|
|
||||||
|
|
||||||
.doc-content {
|
|
||||||
padding-top: 0.5em;
|
|
||||||
top: 3.5625em;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
overflow-y: auto; }
|
|
||||||
|
|
||||||
.doc-sidebar {
|
|
||||||
position: fixed;
|
|
||||||
padding-top: 0.5em;
|
|
||||||
top: 3.5625em;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
overflow-y: auto; }
|
|
||||||
.doc-sidebar ul {
|
|
||||||
list-style-type: none;
|
|
||||||
padding-left: 1em; }
|
|
||||||
.doc-sidebar ul.current {
|
|
||||||
padding-left: 0; }
|
|
||||||
.doc-sidebar li {
|
|
||||||
padding-top: 0.2em;
|
|
||||||
padding-bottom: 0.2em; }
|
|
||||||
|
|
||||||
.admonition {
|
|
||||||
border-radius: 0.3em;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
background-color: #f8f9fa; }
|
|
||||||
.admonition .admonition-title {
|
|
||||||
color: #ff7efd; }
|
|
||||||
|
|
||||||
.topic {
|
|
||||||
border-radius: 0.3em;
|
|
||||||
background-color: #f8f9fa; }
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0; }
|
|
||||||
|
|
||||||
.btn-success {
|
|
||||||
background-color: #008d06;
|
|
||||||
border-color: #008d06; }
|
|
||||||
.btn-success:hover {
|
|
||||||
background-color: #f48400;
|
|
||||||
border-color: #f48400; }
|
|
||||||
|
|
||||||
/*# sourceMappingURL=instaloader.css.map */
|
|
||||||
Vendored
-22
@@ -1,22 +0,0 @@
|
|||||||
$(function () {
|
|
||||||
$('[data-toggle="tooltip"]').tooltip();
|
|
||||||
|
|
||||||
$('.doc-sidebar > ul > li.current').attr("id", "localtoc");
|
|
||||||
$('#localtoc ul').addClass("nav flex-column");
|
|
||||||
$('#localtoc ul li').addClass("nav-item");
|
|
||||||
$('#localtoc ul li a').addClass("nav-link");
|
|
||||||
$('.doc-content').scrollspy({target: '#localtoc'});
|
|
||||||
|
|
||||||
const top_href = '#' + $('.section:first').attr("id");
|
|
||||||
$('#localtoc > a.current').attr("href", top_href);
|
|
||||||
|
|
||||||
$('#navbarToc a').on("click", function () {
|
|
||||||
const href = $(this).attr("href");
|
|
||||||
if (href === '#') {
|
|
||||||
window.location.href = top_href;
|
|
||||||
} else {
|
|
||||||
window.location.href = href;
|
|
||||||
}
|
|
||||||
$('#navbarToc').modal('hide');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Vendored
-117
@@ -1,117 +0,0 @@
|
|||||||
@import 'bootstrap-4.1.3.min.css';
|
|
||||||
|
|
||||||
$color_instaloader_main: #008d06;
|
|
||||||
$color_instaloader_accent: #f48400;
|
|
||||||
$color_instaloader_hyper: #ff7efd;
|
|
||||||
$color_normal_text: #212529;
|
|
||||||
$color_light_bg: #f8f9fa;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: $color_instaloader_main;
|
|
||||||
|
|
||||||
&:hover, &.current, &.active {
|
|
||||||
color: $color_instaloader_accent;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.current, &.active {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.doc-sidebar, .modal-body {
|
|
||||||
a:hover {
|
|
||||||
text-decoration: none
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
color: $color_normal_text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlight {
|
|
||||||
border-radius: 0.3em;
|
|
||||||
|
|
||||||
pre {
|
|
||||||
padding: 0.7em;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.c1 {
|
|
||||||
color: $color_instaloader_main
|
|
||||||
}
|
|
||||||
|
|
||||||
.k, .kn, .ow {
|
|
||||||
color: $color_instaloader_accent;
|
|
||||||
font-weight: bold
|
|
||||||
}
|
|
||||||
|
|
||||||
.nb {
|
|
||||||
color: $color_instaloader_accent
|
|
||||||
}
|
|
||||||
|
|
||||||
.s1, .s2, .mi {
|
|
||||||
color: $color_instaloader_hyper
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.doc-content {
|
|
||||||
padding-top: 0.5em;
|
|
||||||
top: 3.5625em;
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.doc-sidebar {
|
|
||||||
position: fixed;
|
|
||||||
padding-top: 0.5em;
|
|
||||||
top: 3.5625em;
|
|
||||||
bottom: 0;
|
|
||||||
right: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style-type: none;
|
|
||||||
padding-left: 1em;
|
|
||||||
|
|
||||||
&.current {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
padding-top: 0.2em;
|
|
||||||
padding-bottom: 0.2em;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.admonition {
|
|
||||||
border-radius: 0.3em;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
background-color: $color_light_bg;
|
|
||||||
|
|
||||||
.admonition-title {
|
|
||||||
color: $color_instaloader_hyper;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.topic {
|
|
||||||
border-radius: 0.3em;
|
|
||||||
background-color: $color_light_bg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
padding-top: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-success {
|
|
||||||
background-color: $color_instaloader_main;
|
|
||||||
border-color: $color_instaloader_main;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $color_instaloader_accent;
|
|
||||||
border-color: $color_instaloader_accent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
+47
-82
@@ -1,9 +1,6 @@
|
|||||||
{% extends "!layout.html" %}
|
{% extends "!layout.html" %}
|
||||||
|
|
||||||
{% set html5_doctype = True %}
|
{% set html5_doctype = True %}
|
||||||
|
{% set script_files = script_files + ['_static/instaloaderdoc.js'] %}
|
||||||
{% set script_files = script_files + ['_static/bootstrap-4.1.3.bundle.min.js', '_static/instaloader.js'] %}
|
|
||||||
|
|
||||||
{% block htmltitle %}
|
{% block htmltitle %}
|
||||||
{% if pagename == "index" %}
|
{% if pagename == "index" %}
|
||||||
<title>{{ title|striptags|e }}</title>
|
<title>{{ title|striptags|e }}</title>
|
||||||
@@ -11,7 +8,6 @@
|
|||||||
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
|
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrahead %}
|
{% block extrahead %}
|
||||||
{{ super() }}
|
{{ super() }}
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
@@ -22,97 +18,66 @@
|
|||||||
<link rel="canonical" href="https://instaloader.github.io/{{ pagename }}.html"/>
|
<link rel="canonical" href="https://instaloader.github.io/{{ pagename }}.html"/>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}
|
{% block header %}
|
||||||
<nav class="navbar navbar-expand-sm navbar-light bg-light fixed-top border-bottom">
|
<nav class="navbar navbar-light navbar-expand bg-light border-bottom">
|
||||||
<a class="navbar-brand" href="{{ pathto(master_doc) }}">
|
<a class="navbar-brand" href="{{ pathto(master_doc) }}">
|
||||||
<img src="{{ pathto('_static/' + logo, 1) }}" width="30" height="30" class="d-inline-block align-top" alt="logo">
|
<img src="{{ pathto('_static/logo.png', 1) }}" width="30" height="30" class="d-inline-block align-top" alt="logo">
|
||||||
Instaloader
|
<span class="ml-3">Instaloader</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="navbar-nav mr-auto">
|
<ul class="navbar-nav mr-auto">
|
||||||
<li class="nav-item ml-0 ml-sm-2 ml-md-0">
|
<li class="nav-item ml-0 ml-sm-2">
|
||||||
<a target="_blank" class="nav-link"
|
<a class="nav-link"
|
||||||
href="https://github.com/instaloader/instaloader/releases/tag/v{{ current_release }}"
|
href="https://github.com/instaloader/instaloader/releases/tag/v{{ current_release }}"
|
||||||
title="Released on {{ current_release_date }}" data-toggle="tooltip">v<b>{{ current_release }}</b></a>
|
title="Released on {{ current_release_date }}" data-toggle="tooltip">
|
||||||
|
v<b>{{ current_release }}</b>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarLinks"
|
<ul class="navbar-nav ml-auto d-none d-sm-flex">
|
||||||
aria-controls="navbarLinks" aria-expanded="false" aria-label="Toggle navigation">
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://github.com/instaloader/instaloader/issues">
|
||||||
|
Issues
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://github.com/instaloader/instaloader/releases">
|
||||||
|
Releases
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="https://github.com/instaloader/instaloader">
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button class="d-inline-block d-md-none btn btn-outline-primary btn-sm" type="button"
|
||||||
|
data-toggle="collapse" data-target="#instaloaderdocSidebar" aria-controls="instaloaderdocSidebar"
|
||||||
|
aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<div class="collapse navbar-collapse" id="navbarLinks">
|
|
||||||
<ul class="navbar-nav ml-auto">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a target="_blank" class="nav-link" href="https://github.com/instaloader/instaloader/issues">Issues</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a target="_blank" class="nav-link" href="https://github.com/instaloader/instaloader/releases">Releases</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a target="_blank" class="nav-link" href="https://github.com/instaloader/instaloader">GitHub</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item d-sm-none d-lg-block">
|
|
||||||
<a target="_blank" class="nav-link" href="https://saythanks.io/to/aandergr">Say Thanks!</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<form class="form-inline" action="search.html" method="get">
|
|
||||||
<div class="input-group my-2 my-sm-0 ml-2">
|
|
||||||
<input class="form-control" type="search" placeholder="Search" aria-label="Search" name="q">
|
|
||||||
<div class="input-group-append">
|
|
||||||
<button class="btn btn-success btn-small" type="submit">Go</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{% if pagename != "index" %}
|
|
||||||
<button type="button" class="btn btn-success d-md-none btn-small ml-2" data-toggle="modal"
|
|
||||||
data-target="#navbarToc">
|
|
||||||
TOC
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
</nav>
|
</nav>
|
||||||
{% if pagename != "index" %}
|
|
||||||
<div class="modal fade" id="navbarToc" tabindex="-1" role="dialog"
|
|
||||||
aria-hidden="true" aria-labelledby="navbarTocTitle">
|
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="navbarTocTitle">Contents</h5>
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
{{ toctree(maxdepth=-1) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block relbar1 %}{% endblock %}
|
{% block relbar1 %}{% endblock %}
|
||||||
{% block relbar2 %}{% endblock %}
|
{% block relbar2 %}{% endblock %}
|
||||||
{% block footer %}{% endblock %}
|
{% block footer %}{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid row">
|
<div class="container-fluid">
|
||||||
<div class="col-12 {% if pagename != "index" %}col-md-9 col-xl-10{% endif %} doc-content">
|
<div class="row">
|
||||||
{% block body %}{% endblock %}
|
<div class="instaloaderdoc-sidebar collapse d-md-block col-md-3 col-xl-2 bg-light"
|
||||||
</div>
|
id="instaloaderdocSidebar">
|
||||||
{% if pagename != "index" %}
|
<nav role="navigation" aria-label="Table of Contents">
|
||||||
<div class="d-none col-md-3 col-xl-2 bg-light doc-sidebar d-md-flex flex-column border-left">
|
<form action="{{ pathto('search') }}" method="get" role="search" class="border-bottom mr-2 pb-2 mb-2 px-1">
|
||||||
|
<label class="sr-only" for="searchQueryInput">Search</label>
|
||||||
<h3>{{ _('Table of Contents') }}</h3>
|
<input type="search" name="q" id="searchQueryInput" class="form-control my-2"
|
||||||
{{ toctree(maxdepth=-1) }}
|
placeholder="Search..." aria-label="Search">
|
||||||
|
</form>
|
||||||
<div class="d-flex justify-content-between small mt-auto mb-2">
|
<h4>Contents</h4>
|
||||||
<a class="reference internal{% if pagename == "genindex" %} current{% endif %}" href="genindex.html">Index</a>
|
{{ toctree(maxdepth=-1) }}
|
||||||
{% if pagename != "genindex" and pagename != "search" %}
|
</nav>
|
||||||
<a class="reference external"
|
|
||||||
href="https://github.com/instaloader/instaloader/edit/master/docs/{{ pagename }}.rst">Edit this page</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
<main class="col-12 col-md-9 col-xl-10 pt-2" role="main">
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
+16
-134
@@ -12,11 +12,8 @@ Python Module :mod:`instaloader`
|
|||||||
|
|
||||||
.. highlight:: python
|
.. highlight:: python
|
||||||
|
|
||||||
.. contents::
|
|
||||||
:backlinks: none
|
|
||||||
|
|
||||||
Instaloader exposes its internally used methods and structures as a Python
|
Instaloader exposes its internally used methods and structures as a Python
|
||||||
module, making it a **powerful and intuitive Python API for Instagram**,
|
module, making it a powerful and intuitive Python API for Instagram,
|
||||||
allowing to further customize obtaining media and metadata.
|
allowing to further customize obtaining media and metadata.
|
||||||
|
|
||||||
Start with getting an instance of :class:`Instaloader`::
|
Start with getting an instance of :class:`Instaloader`::
|
||||||
@@ -37,7 +34,7 @@ picture, video or sidecar (set of multiple pictures/videos) posted in a user's
|
|||||||
profile. :class:`Instaloader` provides methods to iterate over Posts from a
|
profile. :class:`Instaloader` provides methods to iterate over Posts from a
|
||||||
certain source::
|
certain source::
|
||||||
|
|
||||||
for post in L.get_hashtag_posts('cat'):
|
for post in instaloader.Hashtag.from_name(L.context, 'cat').get_posts():
|
||||||
# post is an instance of instaloader.Post
|
# post is an instance of instaloader.Post
|
||||||
L.download_post(post, target='#cat')
|
L.download_post(post, target='#cat')
|
||||||
|
|
||||||
@@ -63,7 +60,7 @@ certain source::
|
|||||||
- :meth:`Instaloader.get_explore_posts`
|
- :meth:`Instaloader.get_explore_posts`
|
||||||
Media that is suggested by Instagram to explore.
|
Media that is suggested by Instagram to explore.
|
||||||
|
|
||||||
- :meth:`Instaloader.get_hashtag_posts`
|
- :meth:`Hashtag.get_posts`
|
||||||
Media associated with given hashtag.
|
Media associated with given hashtag.
|
||||||
|
|
||||||
With the :class:`Profile` class, Instaloader also makes it easy to access
|
With the :class:`Profile` class, Instaloader also makes it easy to access
|
||||||
@@ -85,141 +82,26 @@ metadata of a Profile. :class:`Profile` instances can be created with:
|
|||||||
- :meth:`Profile.get_followers`
|
- :meth:`Profile.get_followers`
|
||||||
Profiles that follow given user.
|
Profiles that follow given user.
|
||||||
|
|
||||||
- :attr:`Post.owner_profile`, :attr:`Story.owner_profile` and :attr:`StoryItem.owner_profile`
|
- :attr:`Post.owner_profile`, :attr:`Story.owner_profile`, :attr:`StoryItem.owner_profile`
|
||||||
Owner profile of particular object.
|
Owner profile of particular object.
|
||||||
|
|
||||||
- :meth:`Post.get_likes`
|
- :meth:`Post.get_likes`
|
||||||
Profiles that liked a given :class:`Post`
|
Profiles that liked a given :class:`Post`
|
||||||
|
|
||||||
- :attr:`PostComment.owner` attribute for comment in :meth:`Post.get_comments`
|
- :attr:`PostComment.owner`
|
||||||
Profile of a Post comment.
|
Profile of a Post comment.
|
||||||
|
|
||||||
A reference of the many methods provided by the :mod:`instaloader` module is
|
For a list of a few code examples that use the Instaloader module, see
|
||||||
provided in the remainder of this document.
|
:ref:`codesnippets`.
|
||||||
|
|
||||||
For a list of real code examples using the Instaloader module for advanced
|
The reference of the classes and functions provided by the :mod:`instaloader` module is
|
||||||
tasks, see :ref:`codesnippets`.
|
divided into the following subsections:
|
||||||
|
|
||||||
``Instaloader`` (Main Class)
|
.. toctree::
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
:maxdepth: 2
|
||||||
|
|
||||||
.. autoclass:: Instaloader
|
module/instaloader
|
||||||
:no-show-inheritance:
|
module/structures
|
||||||
|
module/nodeiterator
|
||||||
Instagram Structures
|
module/instaloadercontext
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
module/exceptions
|
||||||
|
|
||||||
Posts
|
|
||||||
"""""
|
|
||||||
|
|
||||||
.. autoclass:: Post
|
|
||||||
:no-show-inheritance:
|
|
||||||
|
|
||||||
Additionally, the following trivial structures are defined:
|
|
||||||
|
|
||||||
.. autoclass:: PostSidecarNode
|
|
||||||
:no-show-inheritance:
|
|
||||||
|
|
||||||
.. autoclass:: PostComment
|
|
||||||
:no-show-inheritance:
|
|
||||||
|
|
||||||
.. autoclass:: PostCommentAnswer
|
|
||||||
:no-show-inheritance:
|
|
||||||
|
|
||||||
.. autoclass:: PostLocation
|
|
||||||
:no-show-inheritance:
|
|
||||||
|
|
||||||
User Stories
|
|
||||||
""""""""""""
|
|
||||||
|
|
||||||
.. autoclass:: Story
|
|
||||||
:no-show-inheritance:
|
|
||||||
|
|
||||||
.. autoclass:: StoryItem
|
|
||||||
:no-show-inheritance:
|
|
||||||
|
|
||||||
Highlights
|
|
||||||
""""""""""
|
|
||||||
|
|
||||||
.. autoclass:: Highlight
|
|
||||||
:no-show-inheritance:
|
|
||||||
:inherited-members:
|
|
||||||
|
|
||||||
Bases: :class:`Story`
|
|
||||||
|
|
||||||
.. versionadded:: 4.1
|
|
||||||
|
|
||||||
Profiles
|
|
||||||
""""""""
|
|
||||||
|
|
||||||
.. autoclass:: Profile
|
|
||||||
:no-show-inheritance:
|
|
||||||
|
|
||||||
TopSearchResults
|
|
||||||
""""""""""""""""
|
|
||||||
|
|
||||||
.. autoclass:: TopSearchResults
|
|
||||||
:no-show-inheritance:
|
|
||||||
|
|
||||||
.. versionadded:: 4.3
|
|
||||||
|
|
||||||
Loading and Saving
|
|
||||||
""""""""""""""""""
|
|
||||||
|
|
||||||
:class:`Post`, :class:`StoryItem` and :class:`Profile` can be saved and loaded
|
|
||||||
to/from JSON files.
|
|
||||||
|
|
||||||
.. autofunction:: load_structure_from_file
|
|
||||||
|
|
||||||
.. autofunction:: save_structure_to_file
|
|
||||||
|
|
||||||
Exceptions
|
|
||||||
^^^^^^^^^^
|
|
||||||
|
|
||||||
.. currentmodule:: instaloader.exceptions
|
|
||||||
|
|
||||||
.. autoexception:: InstaloaderException
|
|
||||||
:no-show-inheritance:
|
|
||||||
|
|
||||||
.. autoexception:: ConnectionException
|
|
||||||
|
|
||||||
.. currentmodule:: instaloader
|
|
||||||
|
|
||||||
.. autoexception:: QueryReturnedBadRequestException
|
|
||||||
|
|
||||||
.. autoexception:: 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
|
|
||||||
|
|
||||||
``InstaloaderContext`` (Low-level functions)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
.. autoclass:: InstaloaderContext
|
|
||||||
:no-show-inheritance:
|
|
||||||
|
|||||||
+111
-37
@@ -39,6 +39,16 @@ To later **update your local copy** of that profiles, you may run
|
|||||||
If :option:`--fast-update` is given, Instaloader stops when arriving at the
|
If :option:`--fast-update` is given, Instaloader stops when arriving at the
|
||||||
first already-downloaded picture.
|
first already-downloaded picture.
|
||||||
|
|
||||||
|
Alternatively, you can use :option:`--latest-stamps` to have Instaloader store
|
||||||
|
the time each profile was last downloaded and only download newer media:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
instaloader --latest-stamps -- profile [profile ...]
|
||||||
|
|
||||||
|
With this option it's possible to move or delete downloaded media and still keep
|
||||||
|
the archive updated.
|
||||||
|
|
||||||
When updating profiles, Instaloader
|
When updating profiles, Instaloader
|
||||||
automatically **detects profile name changes** and renames the target directory
|
automatically **detects profile name changes** and renames the target directory
|
||||||
accordingly.
|
accordingly.
|
||||||
@@ -51,7 +61,7 @@ invoke it with
|
|||||||
instaloader --login=your_username profile [profile ...]
|
instaloader --login=your_username profile [profile ...]
|
||||||
|
|
||||||
When logging in, Instaloader **stores the session cookies** in a file in your
|
When logging in, Instaloader **stores the session cookies** in a file in your
|
||||||
temporary directory, which will be reused later the next time :option:`--login`
|
home directory, which will be reused later the next time :option:`--login`
|
||||||
is given. So you can download private profiles **non-interactively** when you
|
is given. So you can download private profiles **non-interactively** when you
|
||||||
already have a valid session cookie file.
|
already have a valid session cookie file.
|
||||||
|
|
||||||
@@ -65,7 +75,7 @@ What to Download
|
|||||||
Instaloader supports the following targets:
|
Instaloader supports the following targets:
|
||||||
|
|
||||||
- ``profile``
|
- ``profile``
|
||||||
Public profile, or private profile with :option:`--login`.
|
Public profile, or private profile with :ref:`login<login>`.
|
||||||
|
|
||||||
If an already-downloaded profile has been renamed, Instaloader automatically
|
If an already-downloaded profile has been renamed, Instaloader automatically
|
||||||
finds it by its unique ID and renames the folder accordingly.
|
finds it by its unique ID and renames the folder accordingly.
|
||||||
@@ -77,39 +87,50 @@ Instaloader supports the following targets:
|
|||||||
instructs Instaloader to also **download the user's stories**,
|
instructs Instaloader to also **download the user's stories**,
|
||||||
|
|
||||||
- :option:`--highlights`
|
- :option:`--highlights`
|
||||||
to **download highlights of each profile that is downloaded**, and
|
to **download the highlights of that profile**,
|
||||||
|
|
||||||
- :option:`--tagged`
|
- :option:`--tagged`
|
||||||
to **download posts where the user is tagged**.
|
to **download posts where the user is tagged**, and
|
||||||
|
|
||||||
|
- :option:`--reels`
|
||||||
|
to **download Reels videos**.
|
||||||
|
|
||||||
|
- :option:`--igtv`
|
||||||
|
to **download IGTV videos**.
|
||||||
|
|
||||||
- ``"#hashtag"``
|
- ``"#hashtag"``
|
||||||
Posts with a certain **hashtag** (the quotes are usually necessary),
|
Posts with a certain **hashtag** (the quotes are usually necessary). Requires :ref:`login<login>`.
|
||||||
|
|
||||||
- ``%location id``
|
- ``%location id``
|
||||||
Posts tagged with a given location; the location ID is the numerical ID
|
Posts tagged with a given location; the location ID is the numerical ID
|
||||||
Instagram labels a location with (e.g.
|
Instagram labels a location with (e.g.
|
||||||
\https://www.instagram.com/explore/locations/**362629379**/plymouth-naval-memorial/).
|
\https://www.instagram.com/explore/locations/**362629379**/plymouth-naval-memorial/).
|
||||||
Requires :option:`--login`.
|
Requires :ref:`login<login>`.
|
||||||
|
|
||||||
.. versionadded:: 4.2
|
.. versionadded:: 4.2
|
||||||
|
|
||||||
- ``:stories``
|
- ``:stories``
|
||||||
The currently-visible **stories** of your followees (requires
|
The currently-visible **stories** of your followees (requires
|
||||||
:option:`--login`),
|
:ref:`login<login>`).
|
||||||
|
|
||||||
- ``:feed``
|
- ``:feed``
|
||||||
Your **feed** (requires :option:`--login`),
|
Your **feed** (requires :ref:`login<login>`).
|
||||||
|
|
||||||
- ``:saved``
|
- ``:saved``
|
||||||
Posts which are marked as **saved** (requires :option:`--login`),
|
Posts which are marked as **saved** (requires :ref:`login<login>`).
|
||||||
|
|
||||||
- ``@profile``
|
- ``@profile``
|
||||||
All profiles that are followed by ``profile``, i.e. the *followees* of
|
All profiles that are followed by ``profile``, i.e. the *followees* of
|
||||||
``profile`` (requires :option:`--login`).
|
``profile`` (requires :ref:`login<login>`).
|
||||||
|
|
||||||
- ``-post``
|
- ``-post``
|
||||||
The single **post** with the given shortcode. Must be preceeded by ``--`` in
|
Replace **post** with the post's shortcode to download single post. Must be preceded by ``--`` in
|
||||||
the argument list to not be mistaken as an option flag.
|
the argument list to not be mistaken as an option flag. For example, to download the post
|
||||||
|
https://www.instagram.com/p/**B_K4CykAOtf**, run the command::
|
||||||
|
|
||||||
|
instaloader -- -B_K4CykAOtf
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.. versionadded:: 4.1
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
@@ -123,7 +144,7 @@ downloads the pictures and videos and their captions. You can specify
|
|||||||
|
|
||||||
- :option:`--geotags`
|
- :option:`--geotags`
|
||||||
**download geotags** of each post and save them as
|
**download geotags** of each post and save them as
|
||||||
Google Maps link (requires :option:`--login`),
|
Google Maps link (requires :ref:`login<login>`),
|
||||||
|
|
||||||
For a reference of all supported command line options, see
|
For a reference of all supported command line options, see
|
||||||
:ref:`command-line-options`.
|
:ref:`command-line-options`.
|
||||||
@@ -142,27 +163,40 @@ target. The default is ``--dirname-pattern={target}``. In the dirname
|
|||||||
pattern, the token ``{target}`` is replaced by the target name, and
|
pattern, the token ``{target}`` is replaced by the target name, and
|
||||||
``{profile}`` is replaced by the owner of the post which is downloaded.
|
``{profile}`` is replaced by the owner of the post which is downloaded.
|
||||||
|
|
||||||
:option:`--filename-pattern` configures the path of the post's files relative
|
:option:`--filename-pattern` configures the path of the post and story's files relative
|
||||||
to the target directory that is specified with :option:`--dirname-pattern`.
|
to the target directory that is specified with :option:`--dirname-pattern`.
|
||||||
The default is ``--filename-pattern={date_utc}_UTC``.
|
The default is ``--filename-pattern={date_utc}_UTC``.
|
||||||
The tokens ``{target}`` and ``{profile}`` are replaced like in the
|
The tokens ``{target}`` and ``{profile}`` are replaced like in the
|
||||||
dirname pattern. The following tokens are defined for usage with
|
dirname pattern.
|
||||||
:option:`--filename-pattern`:
|
|
||||||
|
:option:`--title-pattern` is similar to :option:`--filename-pattern`, but for profile
|
||||||
|
pics, hashtag profile pics, and highlight covers. The default is
|
||||||
|
``{date_utc}_UTC_{typename}`` if :option:`--dirname-pattern` contains ``{target}`` or
|
||||||
|
``{profile}``, or ``{target}_{date_utc}_UTC_{typename}`` if it does not. Some tokens
|
||||||
|
are not supported for this option, see below for details.
|
||||||
|
|
||||||
|
The following tokens are defined for usage with
|
||||||
|
:option:`--filename-pattern` and :option:`--title-pattern`:
|
||||||
|
|
||||||
- ``{target}``
|
- ``{target}``
|
||||||
Target name (as given in Instaloader command line)
|
Target name (as given in Instaloader command line)
|
||||||
|
|
||||||
- ``{profile}`` (same as ``{owner_username}``)
|
- ``{profile}`` (same as ``{owner_username}``)
|
||||||
Owner of the Post / StoryItem.
|
Owner of the Post / StoryItem / ProfilePic. For hashtag profile pics and
|
||||||
|
highlight covers, equivalent to ``{target}``.
|
||||||
|
|
||||||
- ``{owner_id}``
|
- ``{owner_id}``
|
||||||
Unique integer ID of owner profile.
|
Unique integer ID of owner profile. For hashtag profile pics, equivalent to
|
||||||
|
``{target}``.
|
||||||
|
|
||||||
- ``{shortcode}``
|
- ``{shortcode}``
|
||||||
Shortcode (identifier string).
|
Shortcode (identifier string). Not available for :option:`--title-pattern`.
|
||||||
|
|
||||||
- ``{mediaid}``
|
- ``{mediaid}``
|
||||||
Integer representation of shortcode.
|
Integer representation of shortcode. Not available for :option:`--title-pattern`.
|
||||||
|
|
||||||
|
- ``{filename}``
|
||||||
|
Instagram's internal filename.
|
||||||
|
|
||||||
- ``{date_utc}`` (same as ``{date}``)
|
- ``{date_utc}`` (same as ``{date}``)
|
||||||
Creation time in UTC timezone.
|
Creation time in UTC timezone.
|
||||||
@@ -172,6 +206,10 @@ dirname pattern. The following tokens are defined for usage with
|
|||||||
|
|
||||||
{date_utc:%Y-%m-%d_%H-%M-%S}
|
{date_utc:%Y-%m-%d_%H-%M-%S}
|
||||||
|
|
||||||
|
- ``{typename}``
|
||||||
|
Type of media being saved, such as GraphImage, GraphStoryVideo, profile_pic,
|
||||||
|
etc.
|
||||||
|
|
||||||
For example, encode the poster's profile name in the filenames with::
|
For example, encode the poster's profile name in the filenames with::
|
||||||
|
|
||||||
instaloader --filename-pattern={date_utc}_UTC_{profile} "#hashtag"
|
instaloader --filename-pattern={date_utc}_UTC_{profile} "#hashtag"
|
||||||
@@ -189,7 +227,7 @@ Filter Posts
|
|||||||
.. py:currentmodule:: instaloader
|
.. py:currentmodule:: instaloader
|
||||||
|
|
||||||
The options :option:`--post-filter` and :option:`--storyitem-filter`
|
The options :option:`--post-filter` and :option:`--storyitem-filter`
|
||||||
allows to specify criteria that posts or story items have to
|
allow to specify criteria that posts or story items have to
|
||||||
meet to be downloaded. If not given, all posts are downloaded.
|
meet to be downloaded. If not given, all posts are downloaded.
|
||||||
|
|
||||||
The filter string must be a
|
The filter string must be a
|
||||||
@@ -197,11 +235,11 @@ The filter string must be a
|
|||||||
where the attributes from :class:`Post` or
|
where the attributes from :class:`Post` or
|
||||||
:class:`StoryItem` respectively are defined.
|
:class:`StoryItem` respectively are defined.
|
||||||
|
|
||||||
Id est, the following attributes can be used with both
|
The following attributes can be used with both
|
||||||
:option:`--post-filter` and :option:`--storyitem-filter`:
|
:option:`--post-filter` and :option:`--storyitem-filter`:
|
||||||
|
|
||||||
- :attr:`~Post.owner_username` (str), :attr:`~Post.owner_id` (int)
|
- :attr:`~Post.owner_username` (str), :attr:`~Post.owner_id` (int)
|
||||||
Owner profile username / userid.
|
Owner profile username / user ID.
|
||||||
|
|
||||||
- :attr:`~Post.date_utc` (datetime), :attr:`~Post.date_local` (datetime)
|
- :attr:`~Post.date_utc` (datetime), :attr:`~Post.date_local` (datetime)
|
||||||
Creation timestamp. Since :class:`~datetime.datetime` objects can be created
|
Creation timestamp. Since :class:`~datetime.datetime` objects can be created
|
||||||
@@ -210,7 +248,7 @@ Id est, the following attributes can be used with both
|
|||||||
instaloader --post-filter="date_utc <= datetime(2018, 5, 31)" target
|
instaloader --post-filter="date_utc <= datetime(2018, 5, 31)" target
|
||||||
|
|
||||||
- :attr:`~Post.is_video` (bool)
|
- :attr:`~Post.is_video` (bool)
|
||||||
Post/StoryItem is a video. For example, you may skip videos::
|
Whether Post/StoryItem is a video. For example, you may skip videos::
|
||||||
|
|
||||||
instaloader --post-filter="not is_video" target
|
instaloader --post-filter="not is_video" target
|
||||||
|
|
||||||
@@ -221,14 +259,14 @@ Id est, the following attributes can be used with both
|
|||||||
As :option:`--post-filter`, the following attributes can be used additionally:
|
As :option:`--post-filter`, the following attributes can be used additionally:
|
||||||
|
|
||||||
- :attr:`~Post.viewer_has_liked` (bool)
|
- :attr:`~Post.viewer_has_liked` (bool)
|
||||||
Whether user (with :option:`--login`) has liked given post. To download the
|
Whether user (with :ref:`login<login>`) has liked given post. To download the
|
||||||
pictures from your feed that you have liked::
|
pictures from your feed that you have liked::
|
||||||
|
|
||||||
instaloader --login=your_username --post-filter=viewer_has_liked :feed
|
instaloader --login=your_username --post-filter=viewer_has_liked :feed
|
||||||
|
|
||||||
- :attr:`~Post.likes` (int), :attr:`~Post.comments` (int)
|
- :attr:`~Post.likes` (int), :attr:`~Post.comments` (int)
|
||||||
Likes count / Comments count. You might only want to download posts that
|
Likes count / comments count. You might only want to download posts that
|
||||||
either you liked or were liked by many others**::
|
were either liked by yourself or by many others::
|
||||||
|
|
||||||
instaloader --login=your_username --post-filter="likes>100 or viewer_has_liked" profile
|
instaloader --login=your_username --post-filter="likes>100 or viewer_has_liked" profile
|
||||||
|
|
||||||
@@ -258,7 +296,7 @@ along with each post where the Post's caption is saved.
|
|||||||
You can customize what metadata to save for each Post or StoryItem with
|
You can customize what metadata to save for each Post or StoryItem with
|
||||||
:option:`--post-metadata-txt` and :option:`--storyitem-metadata-txt`. The
|
:option:`--post-metadata-txt` and :option:`--storyitem-metadata-txt`. The
|
||||||
default is ``--post-metadata-txt={caption}`` and no storyitem metadata txt.
|
default is ``--post-metadata-txt={caption}`` and no storyitem metadata txt.
|
||||||
These strings are formatted similar as in the :ref:`filename-specification` and
|
These strings are formatted similar as the path patterns described in :ref:`filename-specification` and
|
||||||
the result is saved in text files, unless it is empty.
|
the result is saved in text files, unless it is empty.
|
||||||
|
|
||||||
Specifying these options multiple times results in output having multiple lines,
|
Specifying these options multiple times results in output having multiple lines,
|
||||||
@@ -273,13 +311,47 @@ the post's caption::
|
|||||||
|
|
||||||
instaloader --post-metadata-txt="{likes} likes." <target>
|
instaloader --post-metadata-txt="{likes} likes." <target>
|
||||||
|
|
||||||
Note that with this feature, it is possible to easily and fastly extract
|
Note that with this feature, it is possible to easily and quickly extract
|
||||||
additional metadata of already-downloaded posts, by reimporting their JSON
|
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
|
files. Say, you now also want to export the number of comments the Posts had
|
||||||
when they were downloaded::
|
when they were downloaded::
|
||||||
|
|
||||||
instaloader --post-metadata-txt="{likes} likes, {comments} comments." <target>/*.json.xz
|
instaloader --post-metadata-txt="{likes} likes, {comments} comments." <target>/*.json.xz
|
||||||
|
|
||||||
|
.. _exit_codes:
|
||||||
|
|
||||||
|
Exit codes
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
Different exit codes are used to indicate different kinds of error:
|
||||||
|
|
||||||
|
0
|
||||||
|
No error, all downloads were successful.
|
||||||
|
|
||||||
|
1
|
||||||
|
A non-fatal error happened. One or more posts, or even one or more
|
||||||
|
profiles could not be downloaded, but execution was not stopped. The
|
||||||
|
errors are repeated at the end of the log for easy access.
|
||||||
|
|
||||||
|
2
|
||||||
|
Command-line error. An unrecognized option was passed, or an invalid
|
||||||
|
combination of options, for example. No interaction with Instagram
|
||||||
|
was made.
|
||||||
|
|
||||||
|
3
|
||||||
|
Login error. It was not possible to login. Downloads were not
|
||||||
|
attempted.
|
||||||
|
|
||||||
|
4
|
||||||
|
Fatal download error. Downloads were interrupted and no further
|
||||||
|
attempts were made. Happens when a response with one of the status
|
||||||
|
codes in the :option:`--abort-on` option were passed, or when
|
||||||
|
Instagram logs the user out during downloads.
|
||||||
|
|
||||||
|
5
|
||||||
|
Interrupted by the user. Happens when the user presses Control-C or
|
||||||
|
sends SIGINT to the process.
|
||||||
|
|
||||||
.. _instaloader-as-cronjob:
|
.. _instaloader-as-cronjob:
|
||||||
|
|
||||||
Instaloader as Cronjob
|
Instaloader as Cronjob
|
||||||
@@ -288,17 +360,18 @@ Instaloader as Cronjob
|
|||||||
Instaloader is suitable for running as a cronjob to periodically update your
|
Instaloader is suitable for running as a cronjob to periodically update your
|
||||||
personal Instagram archive. The :option:`--quiet` option disables user
|
personal Instagram archive. The :option:`--quiet` option disables user
|
||||||
interactions and logging of non-error messages. To non-interactively use
|
interactions and logging of non-error messages. To non-interactively use
|
||||||
Instaloader logged-in, create a session file, e.g. in your home directory::
|
Instaloader logged-in, create a session file::
|
||||||
|
|
||||||
instaloader --login=your_username --sessionfile=~/.instaloader-session
|
instaloader --login=your_username
|
||||||
|
|
||||||
Then use the same parameters in your cronjob to load the session and download
|
Then use the same username in your cronjob to load the session and download
|
||||||
the given targets::
|
the given targets::
|
||||||
|
|
||||||
instaloader --login=your_username --sessionfile=~/.instaloader-session --quiet <target> [...]
|
instaloader --login=your_username --quiet target [...]
|
||||||
|
|
||||||
Without :option:`--sessionfile` option, Instaloader saves the session file in
|
Instaloader saves the session file to
|
||||||
a path within your temporary directory.
|
``~/.config/instaloader/session-YOUR-USERNAME``. See
|
||||||
|
:option:`--sessionfile` option for how to override this path.
|
||||||
|
|
||||||
Programming Instaloader
|
Programming Instaloader
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
@@ -309,5 +382,6 @@ Instaloader exposes its internally used methods and structures, making it a
|
|||||||
powerful and intuitive Python API for Instagram, allowing to further customize
|
powerful and intuitive Python API for Instagram, allowing to further customize
|
||||||
obtaining media and metadata.
|
obtaining media and metadata.
|
||||||
|
|
||||||
Also see :ref:`codesnippets`, where we collect example scripts that use
|
Also see :ref:`codesnippets`, where we collect a few example scripts that use
|
||||||
Instaloader to achieve more complex tasks.
|
Instaloader for simple tasks that cannot be done with the command line
|
||||||
|
interface.
|
||||||
|
|||||||
+154
-50
@@ -26,7 +26,7 @@ Targets
|
|||||||
^^^^^^^
|
^^^^^^^
|
||||||
|
|
||||||
Specify a list of targets. For each of these, Instaloader creates a folder and
|
Specify a list of targets. For each of these, Instaloader creates a folder and
|
||||||
stores all posts along with the pictures's captions there.
|
stores all posts along with the pictures' captions there.
|
||||||
|
|
||||||
.. include:: basic-usage.rst
|
.. include:: basic-usage.rst
|
||||||
:start-after: targets-start
|
:start-after: targets-start
|
||||||
@@ -36,7 +36,7 @@ stores all posts along with the pictures's captions there.
|
|||||||
Re-Download the given object
|
Re-Download the given object
|
||||||
|
|
||||||
- ``+args.txt``
|
- ``+args.txt``
|
||||||
Read targets (and options) from given textfile. See :option:`+args.txt`.
|
Read targets (and options) from given text file. See :option:`+args.txt`.
|
||||||
|
|
||||||
What to Download of each Post
|
What to Download of each Post
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
@@ -59,15 +59,15 @@ What to Download of each Post
|
|||||||
|
|
||||||
.. option:: --geotags, -G
|
.. option:: --geotags, -G
|
||||||
|
|
||||||
**Download geotags** when available. Geotags are stored as a text file with
|
Download geotags when available. Geotags are stored as a text file with
|
||||||
the location's name and a Google Maps link. This requires an additional
|
the location's name and a Google Maps link. This requires an additional
|
||||||
request to the Instagram server for each picture. Requires :option:`--login`.
|
request to the Instagram server for each picture. Requires :ref:`login<login>`.
|
||||||
|
|
||||||
.. option:: --comments, -C
|
.. option:: --comments, -C
|
||||||
|
|
||||||
Download and update comments for each post. This requires an additional
|
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
|
request to the Instagram server for each post, which is why it is disabled by
|
||||||
default.
|
default. Requires :ref:`login<login>`.
|
||||||
|
|
||||||
.. option:: --no-captions
|
.. option:: --no-captions
|
||||||
|
|
||||||
@@ -82,6 +82,16 @@ What to Download of each Post
|
|||||||
Template to write in txt file for each StoryItem. See
|
Template to write in txt file for each StoryItem. See
|
||||||
:ref:`metadata-text-files`.
|
:ref:`metadata-text-files`.
|
||||||
|
|
||||||
|
.. option:: --slide
|
||||||
|
|
||||||
|
Download only selected images of a sidecar. You can select single images using their
|
||||||
|
index in the sidecar starting with the leftmost or you can specify a range of images
|
||||||
|
with the following syntax: ``start_index-end_index``. Example:
|
||||||
|
``--slide 1`` will select only the first image, ``--slide last`` only the last one and ``--slide 1-3`` will select only
|
||||||
|
the first three images.
|
||||||
|
|
||||||
|
.. versionadded:: 4.6
|
||||||
|
|
||||||
.. option:: --no-metadata-json
|
.. option:: --no-metadata-json
|
||||||
|
|
||||||
Do not create a JSON file containing the metadata of each post.
|
Do not create a JSON file containing the metadata of each post.
|
||||||
@@ -90,17 +100,9 @@ What to Download of each Post
|
|||||||
|
|
||||||
Do not xz compress JSON files, rather create pretty formatted JSONs.
|
Do not xz compress JSON files, rather create pretty formatted JSONs.
|
||||||
|
|
||||||
|
|
||||||
What to Download of each Profile
|
What to Download of each Profile
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
.. option:: --profile-pic-only, -P
|
|
||||||
|
|
||||||
.. deprecated:: 4.1
|
|
||||||
Use :option:`--no-posts`.
|
|
||||||
|
|
||||||
Only download profile picture.
|
|
||||||
|
|
||||||
.. option:: --no-posts
|
.. option:: --no-posts
|
||||||
|
|
||||||
Do not download regular posts.
|
Do not download regular posts.
|
||||||
@@ -113,13 +115,13 @@ What to Download of each Profile
|
|||||||
|
|
||||||
.. option:: --stories, -s
|
.. option:: --stories, -s
|
||||||
|
|
||||||
Also **download stories** of each profile that is downloaded. Requires
|
Also download stories of each profile that is downloaded. Requires
|
||||||
:option:`--login`.
|
:ref:`login<login>`.
|
||||||
|
|
||||||
.. option:: --highlights
|
.. option:: --highlights
|
||||||
|
|
||||||
Also **download highlights** of each profile that is downloaded. Requires
|
Also download highlights of each profile that is downloaded. Requires
|
||||||
:option:`--login`.
|
:ref:`login<login>`.
|
||||||
|
|
||||||
.. versionadded:: 4.1
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
@@ -129,19 +131,17 @@ What to Download of each Profile
|
|||||||
|
|
||||||
.. versionadded:: 4.1
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
.. option:: --stories-only
|
.. option:: --reels
|
||||||
|
|
||||||
.. deprecated:: 4.1
|
Also download Reels videos.
|
||||||
Use :option:`--stories` :option:`--no-posts`.
|
|
||||||
|
|
||||||
Rather than downloading regular posts of each specified profile, only
|
.. versionadded:: 4.14
|
||||||
download stories. Requires :option:`--login`. Does not imply
|
|
||||||
:option:`--no-profile-pic`.
|
|
||||||
|
|
||||||
.. note::
|
.. option:: --igtv
|
||||||
|
|
||||||
If possible, use ``:stories`` target rather than :option:`--stories-only`
|
Also download IGTV videos.
|
||||||
with all your followees. ``:stories`` uses fewer API requests.
|
|
||||||
|
.. versionadded:: 4.3
|
||||||
|
|
||||||
Which Posts to Download
|
Which Posts to Download
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
@@ -152,6 +152,23 @@ Which Posts to Download
|
|||||||
This flag is recommended when you use Instaloader to update your personal
|
This flag is recommended when you use Instaloader to update your personal
|
||||||
Instagram archive.
|
Instagram archive.
|
||||||
|
|
||||||
|
.. option:: --latest-stamps [STAMPSFILE]
|
||||||
|
|
||||||
|
Works similarly to :option:`--fast-update`, but instead of relying on already
|
||||||
|
downloaded media, the time each profile was downloaded is stored, and only
|
||||||
|
media newer than the last download is fetched. This allows updating your
|
||||||
|
personal Instagram archive while emptying the target directories.
|
||||||
|
|
||||||
|
Only works for media associated with a specific profile, and that is returned
|
||||||
|
in chronological order: profile posts, profile stories, profile IGTV posts
|
||||||
|
and profile tagged posts.
|
||||||
|
|
||||||
|
By default, the information is stored in
|
||||||
|
``~/.config/instaloader/latest-stamps.ini``, but you can specify an
|
||||||
|
alternative location.
|
||||||
|
|
||||||
|
.. versionadded:: 4.8
|
||||||
|
|
||||||
.. option:: --post-filter filter, --only-if filter
|
.. option:: --post-filter filter, --only-if filter
|
||||||
|
|
||||||
Expression that, if given, must evaluate to True for each post to be
|
Expression that, if given, must evaluate to True for each post to be
|
||||||
@@ -169,27 +186,51 @@ Which Posts to Download
|
|||||||
|
|
||||||
.. option:: --count COUNT, -c
|
.. option:: --count COUNT, -c
|
||||||
|
|
||||||
Do not attempt to download more than COUNT posts. Applies only to
|
Do not attempt to download more than COUNT posts. Applies to
|
||||||
``#hashtag``, ``%location id``, and ``:feed``.
|
``#hashtag``, ``%location_id``, ``:feed``, and ``:saved``.
|
||||||
|
|
||||||
|
.. _login:
|
||||||
|
|
||||||
Login (Download Private Profiles)
|
Login (Download Private Profiles)
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
Instaloader can **login to Instagram**. This allows downloading private
|
Instaloader can login to Instagram. This allows downloading private
|
||||||
profiles. To login, pass the :option:`--login` option. Your session cookie (not your
|
profiles. To login, pass the :option:`--login` option. Your session cookie (not your
|
||||||
password!) will be saved to a local file to be reused next time you want
|
password!) will be saved to a local file to be reused next time you want
|
||||||
Instaloader to login.
|
Instaloader to login.
|
||||||
|
|
||||||
|
Instead of :option:`--login`, it is possible to use
|
||||||
|
:option:`--load-cookies` to import a session from a browser.
|
||||||
|
|
||||||
.. option:: --login YOUR-USERNAME, -l YOUR-USERNAME
|
.. option:: --login YOUR-USERNAME, -l YOUR-USERNAME
|
||||||
|
|
||||||
Login name (profile name) for your Instagram account.
|
Login name (profile name) for your Instagram account.
|
||||||
|
|
||||||
|
.. option:: --load-cookies BROWSER-NAME, -b BROWSER-NAME
|
||||||
|
|
||||||
|
Load Instagram session cookies from a browser instead of using Instaloader's own login mechanism.
|
||||||
|
This method requires the `browser_cookie3 <https://github.com/borisbabic/browser_cookie3>`__ library.
|
||||||
|
By default, cookies are loaded from the browser's main profile, but you can specify a custom cookie file using :option:`--cookiefile`.
|
||||||
|
|
||||||
|
Since :option:`--load-cookies` and :option:`--login` are alternative ways to acquire a session, they cannot be used together when initially acquiring a session.
|
||||||
|
|
||||||
|
Supported browsers: Arc, Brave, Chrome, Chromium, Edge, Firefox, LibreWolf, Opera, Opera_GX, Safari, and Vivaldi.
|
||||||
|
|
||||||
|
Once logged in via browser cookies, Instaloader saves the session, allowing you to use :option:`--login` in subsequent runs.
|
||||||
|
|
||||||
|
.. versionadded:: 4.11
|
||||||
|
|
||||||
|
.. option:: --cookiefile COOKIE-FILE, -B COOKIE-FILE
|
||||||
|
|
||||||
|
Specifies a cookie file path for :option:`--load-cookies` to load cookies from a particular browser profile.
|
||||||
|
This option must be used together with :option:`--load-cookies` and does not work on its own.
|
||||||
|
|
||||||
|
.. versionadded:: 4.11
|
||||||
|
|
||||||
.. option:: --sessionfile SESSIONFILE, -f SESSIONFILE
|
.. option:: --sessionfile SESSIONFILE, -f SESSIONFILE
|
||||||
|
|
||||||
Path for loading and storing session key file. Defaults to a path within
|
Path for loading and storing session key file. Defaults to
|
||||||
your temporary directory, encoding your local username and your Instagram
|
``~/.config/instaloader/session-YOUR-USERNAME``.
|
||||||
profile name.
|
|
||||||
|
|
||||||
.. option:: --password YOUR-PASSWORD, -p YOUR-PASSWORD
|
.. option:: --password YOUR-PASSWORD, -p YOUR-PASSWORD
|
||||||
|
|
||||||
@@ -212,16 +253,66 @@ How to Download
|
|||||||
|
|
||||||
.. option:: --filename-pattern FILENAME_PATTERN
|
.. option:: --filename-pattern FILENAME_PATTERN
|
||||||
|
|
||||||
Prefix of filenames, relative to the directory given with
|
Prefix of filenames for posts and stories, relative to the directory given with
|
||||||
:option:`--dirname-pattern`. ``{profile}`` is replaced by the profile name,
|
:option:`--dirname-pattern`. ``{profile}`` is replaced by the profile name,
|
||||||
``{target}`` is replaced by the target you specified, i.e. either ``:feed``,
|
``{target}`` is replaced by the target you specified, i.e. either ``:feed``,
|
||||||
``#hashtag`` or the profile name. Defaults to ``{date_utc}_UTC``.
|
``#hashtag`` or the profile name. Defaults to ``{date_utc}_UTC``.
|
||||||
See :ref:`filename-specification` for a list of supported tokens.
|
See :ref:`filename-specification` for a list of supported tokens.
|
||||||
|
|
||||||
|
.. option:: --title-pattern TITLE_PATTERN
|
||||||
|
|
||||||
|
Prefix of filenames for profile pics, hashtag profile pics, and highlight
|
||||||
|
covers, relative to the directory given with :option:`--dirname-pattern`.
|
||||||
|
Defaults to ``{date_utc}_UTC_{typename}`` if :option:`--dirname-pattern`
|
||||||
|
contains ``{target}`` or ``{profile}``, otherwise defaults to
|
||||||
|
``{target}_{date_utc}_UTC_{typename}``.
|
||||||
|
See :ref:`filename-specification` for a list of supported tokens.
|
||||||
|
|
||||||
|
.. versionadded:: 4.8
|
||||||
|
|
||||||
|
.. option:: --sanitize-paths
|
||||||
|
|
||||||
|
Force sanitization of paths so that the resulting file and directory names
|
||||||
|
are valid on both Windows and Unix.
|
||||||
|
|
||||||
|
.. versionadded:: 4.9
|
||||||
|
|
||||||
|
.. 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 the following targets:
|
||||||
|
- Profile posts,
|
||||||
|
- Profile IGTV posts (:option:`--igtv`),
|
||||||
|
- Profile tagged posts (:option:`--tagged`),
|
||||||
|
- Saved posts (``:saved``),
|
||||||
|
- Hashtags.
|
||||||
|
|
||||||
|
This feature is enabled by default for targets where it is supported;
|
||||||
|
:option:`--resume-prefix` only changes the name of the iterator files.
|
||||||
|
|
||||||
|
To turn this feature off, use :option:`--no-resume`.
|
||||||
|
|
||||||
|
JSON files with resume information are always compressed, regardless of
|
||||||
|
:option:`--no-compress-json`.
|
||||||
|
|
||||||
|
.. 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
|
.. option:: --user-agent USER_AGENT
|
||||||
|
|
||||||
User Agent to use for HTTP requests. Per default, Instaloader pretends being
|
User Agent to use for HTTP requests. Per default, Instaloader pretends being
|
||||||
Chrome/51.
|
Chrome/142 on Linux.
|
||||||
|
|
||||||
.. option:: --max-connection-attempts N
|
.. option:: --max-connection-attempts N
|
||||||
|
|
||||||
@@ -229,20 +320,32 @@ How to Download
|
|||||||
to ``3``. If a connection fails, it can be manually skipped by hitting
|
to ``3``. If a connection fails, it can be manually skipped by hitting
|
||||||
:kbd:`Control-c`. Set this to ``0`` to retry infinitely.
|
:kbd:`Control-c`. Set this to ``0`` to retry infinitely.
|
||||||
|
|
||||||
.. option:: --commit-mode
|
|
||||||
|
|
||||||
Tries to ensure downloaded images avoid corruption in case of unexpected
|
|
||||||
interruption. If the last picture is corrupted, Instaloader will fix the
|
|
||||||
picture the next time it is run.
|
|
||||||
|
|
||||||
.. versionadded:: 4.2
|
|
||||||
|
|
||||||
.. option:: --request-timeout N
|
.. option:: --request-timeout N
|
||||||
|
|
||||||
Seconds to wait before timing out a connection request.
|
Seconds to wait before timing out a connection request. Defaults to 300.
|
||||||
|
|
||||||
.. versionadded:: 4.3
|
.. versionadded:: 4.3
|
||||||
|
|
||||||
|
.. versionchanged:: 4.6
|
||||||
|
Enabled this option by default with a timeout of 300 seconds.
|
||||||
|
|
||||||
|
.. option:: --abort-on STATUS_CODE_LIST
|
||||||
|
|
||||||
|
Comma-separated list of HTTP status codes that cause Instaloader to abort,
|
||||||
|
bypassing all retry logic.
|
||||||
|
|
||||||
|
For example, with ``--abort-on=302,400,429``, Instaloader will stop if a
|
||||||
|
request is responded with a 302 redirect, a Bad Request error, or a Too Many
|
||||||
|
Requests error.
|
||||||
|
|
||||||
|
.. versionadded:: 4.7
|
||||||
|
|
||||||
|
.. option:: --no-iphone
|
||||||
|
|
||||||
|
Do not attempt to download iPhone version of images and videos.
|
||||||
|
|
||||||
|
.. versionadded:: 4.8
|
||||||
|
|
||||||
Miscellaneous Options
|
Miscellaneous Options
|
||||||
^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
@@ -256,18 +359,19 @@ Miscellaneous Options
|
|||||||
|
|
||||||
Read arguments from file `args.txt`, a shortcut to provide arguments from
|
Read arguments from file `args.txt`, a shortcut to provide arguments from
|
||||||
file rather than command-line. This provides a convenient way to hide login
|
file rather than command-line. This provides a convenient way to hide login
|
||||||
info from CLI, and can also be used to simplify managment of long arguments.
|
info from CLI, and can also be used to simplify management of long arguments.
|
||||||
|
You can provide more than one file at once, e.g.: ``+args1.txt +args2.txt``.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Text file should separate arguments with line breaks.
|
Text file should separate arguments with line breaks.
|
||||||
|
|
||||||
args.txt example::
|
`args.txt` example::
|
||||||
|
|
||||||
--login=MYUSERNAME
|
--login=MYUSERNAME
|
||||||
--password=MYPASSWORD
|
--password=MYPASSWORD
|
||||||
--fast-update
|
--fast-update
|
||||||
profile1
|
profile1
|
||||||
profile2
|
profile2
|
||||||
|
|
||||||
.. versionadded:: 4.1
|
.. versionadded:: 4.1
|
||||||
|
|||||||
+18
-34
@@ -28,8 +28,9 @@ Download Posts in a Specific Period
|
|||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
To only download Instagram pictures (and metadata) that are within a specific
|
To only download Instagram pictures (and metadata) that are within a specific
|
||||||
period, you can play around with :func:`~itertools.dropwhile` and
|
period, you can simply use :func:`~itertools.dropwhile` and
|
||||||
:func:`~itertools.takewhile` from :mod:`itertools` like in this snippet.
|
: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
|
.. literalinclude:: codesnippets/121_since_until.py
|
||||||
|
|
||||||
@@ -37,11 +38,19 @@ See also :class:`Post`, :meth:`Instaloader.download_post`.
|
|||||||
|
|
||||||
Discussed in :issue:`121`.
|
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
|
Likes of a Profile / Ghost Followers
|
||||||
------------------------------------
|
------------------------------------
|
||||||
|
|
||||||
To obtain a list of your inactive followers, i.e. followers that did not like
|
To obtain a list of your inactive followers, i.e. followers that did not like
|
||||||
any of your pictures, into a file you can use this approach.
|
any of your pictures, you can use this approach.
|
||||||
|
|
||||||
.. literalinclude:: codesnippets/120_ghost_followers.py
|
.. literalinclude:: codesnippets/120_ghost_followers.py
|
||||||
|
|
||||||
@@ -69,11 +78,11 @@ Discussed in :issue:`56`.
|
|||||||
Only one Post per User
|
Only one Post per User
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
To download only the one most recent post from each user, this snippet creates a
|
To download only the single most recent post per user within a hashtag feed,
|
||||||
:class:`set` that contains the users of which a post has already been
|
this snippet uses a :class:`set` that contains the users of whom a post has
|
||||||
downloaded. While iterating the posts, it checks whether the post's owner
|
already been downloaded. For each post, it checks whether the post's creator is
|
||||||
already is in the set. If not, the post is downloaded from Instagram and the
|
already contained in that set. If not, the post is downloaded from Instagram and
|
||||||
user is added to that set.
|
the user is added to that set.
|
||||||
|
|
||||||
.. literalinclude:: codesnippets/113_only_one_per_user.py
|
.. literalinclude:: codesnippets/113_only_one_per_user.py
|
||||||
|
|
||||||
@@ -85,37 +94,12 @@ Discussed in :issue:`113`.
|
|||||||
Top X Posts of User
|
Top X Posts of User
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
With Instaloader, it is easy to download the few most-liked pictres of a user.
|
With Instaloader, it is easy to download the few most-liked pictures of a user.
|
||||||
|
|
||||||
.. literalinclude:: codesnippets/194_top_x_of_user.py
|
.. literalinclude:: codesnippets/194_top_x_of_user.py
|
||||||
|
|
||||||
Discussed in :issue:`194`.
|
Discussed in :issue:`194`.
|
||||||
|
|
||||||
Upgrade Images by Local Copies
|
|
||||||
------------------------------
|
|
||||||
|
|
||||||
The following script finds local versions of images fetched by Instaloader, in
|
|
||||||
order to upgrade the downloaded images by locally-found versions with better
|
|
||||||
quality. It uses image hashing to identify similar images.
|
|
||||||
|
|
||||||
`updgrade-instaloader-images.py <https://gist.github.com/pavelkryukov/15f93d19a99428a284a8bcec27e0187b>`__ (external link to GitHub Gist)
|
|
||||||
|
|
||||||
Discussed in :issue:`46`.
|
|
||||||
|
|
||||||
Add Captions to Images
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
Instaloader does not modify the downloaded JPEG file. However, one could combine
|
|
||||||
it with an imaging library such as Pillow or PIL to render the caption on
|
|
||||||
Instagram pictures. The following shows an approach.
|
|
||||||
|
|
||||||
.. literalinclude:: codesnippets/110_pil_captions.py
|
|
||||||
|
|
||||||
See also :attr:`Post.caption`, :attr:`Post.url`, :meth:`Post.from_shortcode`,
|
|
||||||
:func:`load_structure_from_file`.
|
|
||||||
|
|
||||||
Discussed in :issue:`110`.
|
|
||||||
|
|
||||||
Metadata JSON Files
|
Metadata JSON Files
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
from io import BytesIO
|
|
||||||
|
|
||||||
from requests import get
|
|
||||||
from PIL import Image, ImageDraw
|
|
||||||
from instaloader import *
|
|
||||||
|
|
||||||
L = Instaloader()
|
|
||||||
|
|
||||||
# Load Post instance
|
|
||||||
post = load_structure_from_file(L.context, '2017-10-01_18-53-03_UTC.json.xz')
|
|
||||||
# or post = Post.from_shortcode(L.context, SHORTCODE)
|
|
||||||
|
|
||||||
# Render caption
|
|
||||||
image = Image.open(BytesIO(get(post.url).content))
|
|
||||||
draw = ImageDraw.Draw(image)
|
|
||||||
color = 'rgb(0, 0, 0)' # black color
|
|
||||||
draw.text((300,100), post.caption.encode('latin1', errors='ignore'), fill=color)
|
|
||||||
|
|
||||||
# Save image
|
|
||||||
image.save('test.jpg')
|
|
||||||
@@ -2,7 +2,7 @@ import instaloader
|
|||||||
|
|
||||||
L = instaloader.Instaloader()
|
L = instaloader.Instaloader()
|
||||||
|
|
||||||
posts = L.get_hashtag_posts('urbanphotography')
|
posts = instaloader.Hashtag.from_name(L.context, 'urbanphotography').get_posts_resumable()
|
||||||
|
|
||||||
users = set()
|
users = set()
|
||||||
|
|
||||||
|
|||||||
@@ -2,26 +2,26 @@ import instaloader
|
|||||||
|
|
||||||
L = instaloader.Instaloader()
|
L = instaloader.Instaloader()
|
||||||
|
|
||||||
USER = 'your_account'
|
USER = "your_account"
|
||||||
PROFILE = USER
|
PROFILE = USER
|
||||||
|
|
||||||
# Your preferred way of logging in:
|
# Load session previously saved with `instaloader -l USERNAME`:
|
||||||
L.load_session_from_file(USER)
|
L.load_session_from_file(USER)
|
||||||
|
|
||||||
profile = instaloader.Profile.from_username(L.context, PROFILE)
|
profile = instaloader.Profile.from_username(L.context, PROFILE)
|
||||||
|
|
||||||
likes = set()
|
likes = set()
|
||||||
print('Fetching likes of all posts of profile {}.'.format(profile.username))
|
print("Fetching likes of all posts of profile {}.".format(profile.username))
|
||||||
for post in profile.get_posts():
|
for post in profile.get_posts():
|
||||||
print(post)
|
print(post)
|
||||||
likes = likes | set(post.get_likes())
|
likes = likes | set(post.get_likes())
|
||||||
|
|
||||||
print('Fetching followers of profile {}.'.format(profile.username))
|
print("Fetching followers of profile {}.".format(profile.username))
|
||||||
followers = set(profile.get_followers())
|
followers = set(profile.get_followers())
|
||||||
|
|
||||||
ghosts = followers - likes
|
ghosts = followers - likes
|
||||||
|
|
||||||
print('Storing ghosts into file.')
|
print("Storing ghosts into file.")
|
||||||
with open('/YOUR PATH/inactive-users.txt', 'w') as f:
|
with open("inactive-users.txt", 'w') as f:
|
||||||
for ghost in ghosts:
|
for ghost in ghosts:
|
||||||
print(ghost.username, file=f)
|
print(ghost.username, file=f)
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ import instaloader
|
|||||||
|
|
||||||
L = instaloader.Instaloader()
|
L = instaloader.Instaloader()
|
||||||
|
|
||||||
posts = L.get_hashtag_posts('urbanphotography')
|
posts = instaloader.Profile.from_username(L.context, "instagram").get_posts()
|
||||||
# or
|
|
||||||
# posts = instaloader.Profile.from_username(L.context, PROFILE).get_posts()
|
|
||||||
|
|
||||||
SINCE = datetime(2015, 5, 1)
|
SINCE = datetime(2015, 5, 1)
|
||||||
UNTIL = datetime(2015, 3, 1)
|
UNTIL = datetime(2015, 3, 1)
|
||||||
|
|
||||||
for post in takewhile(lambda p: p.date > UNTIL, dropwhile(lambda p: p.date > SINCE, posts)):
|
for post in takewhile(lambda p: p.date > UNTIL, dropwhile(lambda p: p.date > SINCE, posts)):
|
||||||
print(post.date)
|
print(post.date)
|
||||||
L.download_post(post, '#urbanphotography')
|
L.download_post(post, "instagram")
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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(), None)
|
||||||
|
if default_cookiefile:
|
||||||
|
cookiefiles = glob(expanduser(default_cookiefile))
|
||||||
|
else:
|
||||||
|
# Check Firefox 147+ XDG path, then legacy
|
||||||
|
cookiefiles = glob(expanduser("~/.config/mozilla/firefox/*/cookies.sqlite")) or \
|
||||||
|
glob(expanduser("~/.mozilla/firefox/*/cookies.sqlite"))
|
||||||
|
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(f"file:{cookiefile}?immutable=1", uri=True)
|
||||||
|
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_resumable()
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
from glob import glob
|
|
||||||
from os.path import expanduser
|
|
||||||
from sqlite3 import connect
|
|
||||||
|
|
||||||
from instaloader import ConnectionException, Instaloader
|
|
||||||
|
|
||||||
# FIREFOXCOOKIEFILE = "/home/alex/.mozilla/firefox/l96w6b90.default/cookies.sqlite"
|
|
||||||
FIREFOXCOOKIEFILE = glob(expanduser("~/.mozilla/firefox/*.default/cookies.sqlite"))[0]
|
|
||||||
|
|
||||||
instaloader = Instaloader(max_connection_attempts=1)
|
|
||||||
instaloader.context._session.cookies.update(connect(FIREFOXCOOKIEFILE)
|
|
||||||
.execute("SELECT name, value FROM moz_cookies "
|
|
||||||
"WHERE baseDomain='instagram.com'"))
|
|
||||||
|
|
||||||
try:
|
|
||||||
username = instaloader.test_login()
|
|
||||||
if not username:
|
|
||||||
raise ConnectionException()
|
|
||||||
except ConnectionException:
|
|
||||||
raise SystemExit("Cookie import failed. Are you logged in successfully in Firefox?")
|
|
||||||
|
|
||||||
instaloader.context.username = username
|
|
||||||
instaloader.save_session_to_file()
|
|
||||||
+54
-332
@@ -1,366 +1,88 @@
|
|||||||
#!/usr/bin/env python3
|
# Configuration file for the Sphinx documentation builder.
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
#
|
#
|
||||||
# Instaloader documentation build configuration file, created by
|
# For the full list of built-in configuration values, see the documentation:
|
||||||
# sphinx-quickstart on Fri Aug 25 15:38:26 2017.
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||||
#
|
|
||||||
# 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 os
|
||||||
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import typing
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('..'))
|
sys.path.insert(0, os.path.abspath('..'))
|
||||||
|
|
||||||
import docs.sphinx_autodoc_typehints as sphinx_autodoc_typehints
|
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||||
|
|
||||||
# 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)}
|
|
||||||
|
|
||||||
extlinks = {'issue': ('https://github.com/instaloader/instaloader/issues/%s', 'issue #')}
|
|
||||||
|
|
||||||
# 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'
|
project = 'Instaloader'
|
||||||
copyright = '2017, Alexander Graf and Andre Koch-Kramer'
|
copyright = '2023, Alexander Graf and Andre Koch-Kramer'
|
||||||
author = '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
|
# -- General configuration ---------------------------------------------------
|
||||||
# |version| and |release|, also used in various other places throughout the
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
# 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
|
extensions = [
|
||||||
# for a list of supported languages.
|
"sphinx.ext.autodoc",
|
||||||
#
|
"sphinx.ext.extlinks",
|
||||||
# This is also used if you do content translation via gettext catalogs.
|
"sphinx.ext.githubpages",
|
||||||
# Usually you set "language" from the command line for these cases.
|
"sphinx.ext.intersphinx",
|
||||||
language = "en"
|
"sphinxcontrib.jquery",
|
||||||
|
]
|
||||||
|
|
||||||
# There are two options for replacing |today|: either, you set today to some
|
# autodoc options
|
||||||
# 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
|
autodoc_default_options = {
|
||||||
# directories to ignore when looking for source files.
|
'show-inheritance': True,
|
||||||
# This patterns also effect to html_static_path and html_extra_path
|
'members': True,
|
||||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
'undoc-members': True,
|
||||||
|
|
||||||
# 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
|
# extlinks options
|
||||||
# (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
|
extlinks = {
|
||||||
# the title page.
|
'issue': (
|
||||||
#
|
'https://github.com/instaloader/instaloader/issues/%s',
|
||||||
# latex_logo = None
|
'Issue #%s'
|
||||||
|
),
|
||||||
|
'example': (
|
||||||
|
'https://raw.githubusercontent.com/instaloader/instaloader/master/docs/codesnippets/%s',
|
||||||
|
'Example %s'
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
# intersphinx options
|
||||||
# not chapters.
|
|
||||||
#
|
|
||||||
# latex_use_parts = False
|
|
||||||
|
|
||||||
# If true, show page references after internal links.
|
intersphinx_mapping = {
|
||||||
#
|
'python': ('https://docs.python.org/3', None),
|
||||||
# latex_show_pagerefs = False
|
'requests': ('https://requests.readthedocs.io/en/latest/', None),
|
||||||
|
}
|
||||||
|
|
||||||
# If true, show URL addresses after external links.
|
# other options
|
||||||
#
|
|
||||||
# latex_show_urls = False
|
|
||||||
|
|
||||||
# Documents to append as an appendix to all manuals.
|
templates_path = ['_templates']
|
||||||
#
|
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||||
# latex_appendices = []
|
|
||||||
|
|
||||||
# It false, will not define \strong, \code, itleref, \crossref ... but only
|
# ignore TypeVar as it is currently unsupported by Sphinx
|
||||||
# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
|
nitpick_ignore = [('py:class', 'instaloader.nodeiterator.T')]
|
||||||
# packages.
|
|
||||||
#
|
|
||||||
# latex_keep_old_macro_names = True
|
|
||||||
|
|
||||||
# If false, no module index is generated.
|
# -- Options for HTML output -------------------------------------------------
|
||||||
#
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||||
# latex_domain_indices = True
|
|
||||||
|
|
||||||
|
pygments_style = 'fruity'
|
||||||
|
|
||||||
# -- Options for manual page output ---------------------------------------
|
html_theme = 'basic'
|
||||||
|
html_static_path = ['_static']
|
||||||
|
|
||||||
# One entry per manual page. List of tuples
|
html_logo = "logo.png"
|
||||||
# (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.
|
html_css_files = ['instaloaderdoc.css']
|
||||||
#
|
html_js_files = ['instaloaderdoc.js']
|
||||||
# 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
|
|
||||||
|
|
||||||
|
# get release number
|
||||||
|
|
||||||
current_release = subprocess.check_output(["git", "describe", "--abbrev=0"]).decode("ascii")[1:-1]
|
current_release = subprocess.check_output(["git", "describe", "--abbrev=0"]).decode("ascii")[1:-1]
|
||||||
current_release_date = subprocess.check_output(["git", "log", "-1", "--tags", "--format=%ad", "--date=format:%e %b %Y"]).decode("ascii")[:-1]
|
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}
|
html_context = {'current_release': current_release, 'current_release_date': current_release_date}
|
||||||
|
|
||||||
|
|
||||||
def setup(app):
|
|
||||||
typing.TYPE_CHECKING = True
|
|
||||||
app.add_stylesheet("instaloader.css")
|
|
||||||
app.connect('autodoc-process-signature', sphinx_autodoc_typehints.process_signature)
|
|
||||||
app.connect('autodoc-process-docstring', sphinx_autodoc_typehints.process_docstring)
|
|
||||||
|
|||||||
+79
-48
@@ -11,6 +11,23 @@ Instaloader's development is organized on
|
|||||||
`GitHub <https://github.com/instaloader/instaloader>`__, where Issues and Pull
|
`GitHub <https://github.com/instaloader/instaloader>`__, where Issues and Pull
|
||||||
Requests are discussed.
|
Requests are discussed.
|
||||||
|
|
||||||
|
Contributing to an open source project is an unmatchable opportunity to improve
|
||||||
|
and manifest programming skills. For issues that are good starting points to get
|
||||||
|
involved in Instaloader development, as they do not require deep insight into
|
||||||
|
Instaloader's code base, have a look at our
|
||||||
|
`Issues labelled 'good first issue' <https://github.com/instaloader/instaloader/contribute>`__.
|
||||||
|
|
||||||
|
This document covers several ways of how you can contribute back to Instaloader.
|
||||||
|
|
||||||
|
Answering Questions
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
The easiest way to help out is to answer questions. If you are interested in
|
||||||
|
answering questions regarding Instaloader, good places to begin are
|
||||||
|
|
||||||
|
- `Questions tagged 'instaloader' on Stack Overflow <https://stackoverflow.com/questions/tagged/instaloader>`__,
|
||||||
|
- `Instaloader Issues labeled 'question' <https://github.com/instaloader/instaloader/issues?q=is%3Aissue+is%3Aopen+label%3Aquestion>`__.
|
||||||
|
|
||||||
Reporting Bugs
|
Reporting Bugs
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
@@ -32,41 +49,63 @@ reporting a problem, please keep the following in mind:
|
|||||||
|
|
||||||
- Include all **error messages and tracebacks** in the report.
|
- Include all **error messages and tracebacks** in the report.
|
||||||
|
|
||||||
- If not obvious, describe **which behavior you expected**
|
- Even if it seems obvious, describe **which behavior you expected**
|
||||||
instead of what actually happened.
|
instead of what actually happened.
|
||||||
|
|
||||||
- If you think an issue has been closed accidentally or inappropriately, feel
|
- If we have closed an issue apparently inadvertently or inappropriately, please
|
||||||
free to reopen it.
|
let us know.
|
||||||
|
|
||||||
Writing Code or Improving the Documentation
|
Writing Code or Improving the Documentation
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
|
|
||||||
Changes of the Instaloader source can be proposed as a
|
Improvements of the Instaloader source or its documentation can be proposed as a
|
||||||
`Pull Request <https://github.com/instaloader/instaloader/pulls>`__. There are only
|
`Pull Request <https://github.com/instaloader/instaloader/pulls>`__.
|
||||||
few things to consider:
|
|
||||||
|
|
||||||
- Sometimes, the most current code is not in the ``master`` branch. Check that
|
- Please base your Pull Request on
|
||||||
you forked from the most recent branch.
|
|
||||||
|
|
||||||
- We use `Pylint <https://www.pylint.org/>`__ for error and syntax checking of
|
- ``master``, which will be released with the next minor release, if it is
|
||||||
the source. The file ``.travis.yml`` in the project's root directory
|
|
||||||
shows how it is invoked. Note that sometimes it might be better to disable a
|
|
||||||
warning rather than adapting the code to Pylint's desires.
|
|
||||||
|
|
||||||
- The documentation source is located in the ``docs`` folder. The file
|
- a bug fix that does not require extensive testing,
|
||||||
``cli-options.rst`` is merely an RST-formatted copy of ``instaloader --help``
|
- an improvement to the documentation,
|
||||||
output, of which the source is in ``instaloader/__main__.py``.
|
|
||||||
|
|
||||||
- Feel free to contact us, even if you "only" have Proof-of-Concepts or
|
- ``upcoming/v4.X``, if it is
|
||||||
not-fully integrated changes. They already might be an advance for the
|
|
||||||
project.
|
|
||||||
|
|
||||||
Suggesting Features
|
- a new feature,
|
||||||
-------------------
|
- a bug fix that does require thorough testing before being released to a
|
||||||
|
final version.
|
||||||
|
|
||||||
|
- Keep Pull Requests focused: address a single problem or feature in a
|
||||||
|
self-contained change. Avoid bundling multiple independent fixes, refactorings,
|
||||||
|
or architectural changes into one pull request, as this makes it hard to
|
||||||
|
review, validate, or selectively merge them.
|
||||||
|
|
||||||
|
- Be mindful of backward compatibility: changes should preserve existing intended
|
||||||
|
behavior whenever possible.
|
||||||
|
|
||||||
|
- All Pull Requests are analyzed by `Pylint <https://www.pylint.org/>`__ for
|
||||||
|
error and syntax checking of the source and
|
||||||
|
`Mypy <https://github.com/python/mypy>`__ for type checking. You can run them
|
||||||
|
locally with::
|
||||||
|
|
||||||
|
pylint instaloader
|
||||||
|
mypy -m instaloader
|
||||||
|
|
||||||
|
- Improvements to the documentation are very welcome. The documentation is
|
||||||
|
created with `Sphinx <https://www.sphinx-doc.org/en/2.0/>`__, version 2,
|
||||||
|
and can be build locally using::
|
||||||
|
|
||||||
|
make -C docs html
|
||||||
|
|
||||||
|
- Feel free to create an issue to make sure someone from the Instaloader team
|
||||||
|
agrees that the change might be an improvement, or if you want to discuss
|
||||||
|
your basic proposal, before working on a pull request.
|
||||||
|
|
||||||
|
Proposing Features
|
||||||
|
------------------
|
||||||
|
|
||||||
.. goal-start
|
.. goal-start
|
||||||
|
|
||||||
Instaloader's goal is to mimick the browser's behavior to access the data that
|
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
|
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
|
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
|
common downloading and metadata collection tasks, without changing any of the
|
||||||
@@ -74,10 +113,9 @@ online data.
|
|||||||
|
|
||||||
.. goal-end
|
.. goal-end
|
||||||
|
|
||||||
If you have an idea of how Instaloader should be enhanced, but do not want to
|
Prior spending effort on implementing a new feature, it might be appropriate to
|
||||||
implement the feature yourself, feel free to open a ticket in our
|
clarify how it could fit into the project's scope or discuss implementation
|
||||||
`Issue Tracker <https://github.com/instaloader/instaloader/issues>`__.
|
details. If you feel the need to do so, please create a "feature suggestion".
|
||||||
Please consider the following:
|
|
||||||
|
|
||||||
- Instaloader already has plenty of features. **Check the documentation**
|
- Instaloader already has plenty of features. **Check the documentation**
|
||||||
beforehand to ensure your desired suggestion is not already implemented.
|
beforehand to ensure your desired suggestion is not already implemented.
|
||||||
@@ -86,32 +124,25 @@ Please consider the following:
|
|||||||
an issue suggesting the same or a similar feature, share your thoughts in a
|
an issue suggesting the same or a similar feature, share your thoughts in a
|
||||||
comment there, instead of opening a new issue.
|
comment there, instead of opening a new issue.
|
||||||
|
|
||||||
- If possible, provide us a **use case of the feature**: How could the user
|
- **Motivate the feature**, i.e.
|
||||||
invoke the new function? Which problem would it solve? If new information is
|
|
||||||
obtained, how would it be further processed?
|
|
||||||
|
|
||||||
- If not obvious, briefly motivate how your suggested feature **conforms with
|
- Provide us a **use case of the feature**: How could the user
|
||||||
Instaloader's project goal**.
|
invoke the new function? Which problem would it solve? If new information is
|
||||||
|
obtained, how would it be further processed?
|
||||||
|
|
||||||
- **Be patient**. Naturally, bugs and pull requests have a higher priority than
|
- Describe already-working **alternatives of the feature** and how they
|
||||||
feature suggestions. Keep in mind that this is a free software project, and
|
compare to your proposed feature.
|
||||||
unfortunately we only have limited time to work on it.
|
|
||||||
|
|
||||||
Donations
|
- Briefly describe how your suggested feature **conforms with Instaloader's
|
||||||
---------
|
project goal**.
|
||||||
|
|
||||||
.. donations-start
|
- 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.
|
||||||
|
|
||||||
It is a pleasure for us to share our Instaloader to the world, and we are proud
|
Sponsoring
|
||||||
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.
|
.. include:: sponsors.rst
|
||||||
| `PayPal.me/aandergr <https://www.paypal.me/aandergr>`__
|
:start-after: donations-start
|
||||||
| BTC: 1Nst4LoadeYzrKjJ1DX9CpbLXBYE9RKLwY
|
:end-before: donations-end
|
||||||
|
|
||||||
.. donations-end
|
|
||||||
|
|
||||||
.. (Discussion in :issue:`130`)
|
|
||||||
|
|||||||
+15
-9
@@ -40,12 +40,15 @@ See :ref:`install` for more options on how to install Instaloader.
|
|||||||
- allows **fine-grained customization** of filters and where to store
|
- allows **fine-grained customization** of filters and where to store
|
||||||
downloaded media,
|
downloaded media,
|
||||||
|
|
||||||
|
- automatically **resumes previously-interrupted** download iterations,
|
||||||
|
|
||||||
- is free `open source <https://github.com/instaloader/instaloader>`__
|
- is free `open source <https://github.com/instaloader/instaloader>`__
|
||||||
software written in Python.
|
software written in Python.
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
instaloader [--comments] [--geotags] [--stories] [--highlights] [--tagged]
|
instaloader [--comments] [--geotags]
|
||||||
|
[--stories] [--highlights] [--tagged] [--reels] [--igtv]
|
||||||
[--login YOUR-USERNAME] [--fast-update]
|
[--login YOUR-USERNAME] [--fast-update]
|
||||||
profile | "#hashtag" | %location_id |
|
profile | "#hashtag" | %location_id |
|
||||||
:stories | :feed | :saved
|
:stories | :feed | :saved
|
||||||
@@ -75,13 +78,6 @@ Useful Links
|
|||||||
- `Issue Tracker / Bug Tracker <https://github.com/instaloader/instaloader/issues>`__
|
- `Issue Tracker / Bug Tracker <https://github.com/instaloader/instaloader/issues>`__
|
||||||
- `Version History <https://github.com/instaloader/instaloader/releases>`__
|
- `Version History <https://github.com/instaloader/instaloader/releases>`__
|
||||||
|
|
||||||
Disclaimer
|
|
||||||
----------
|
|
||||||
|
|
||||||
.. include:: ../README.rst
|
|
||||||
:start-after: disclaimer-start
|
|
||||||
:end-before: disclaimer-end
|
|
||||||
|
|
||||||
Contributing
|
Contributing
|
||||||
------------
|
------------
|
||||||
|
|
||||||
@@ -89,10 +85,20 @@ As an open source project, Instaloader heavily depends on the contributions from
|
|||||||
its community. See :ref:`contributing` for how you may help Instaloader to
|
its community. See :ref:`contributing` for how you may help Instaloader to
|
||||||
become an even greater tool.
|
become an even greater tool.
|
||||||
|
|
||||||
.. include:: contributing.rst
|
Supporters
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. include:: sponsors.rst
|
||||||
:start-after: donations-start
|
:start-after: donations-start
|
||||||
:end-before: donations-end
|
:end-before: donations-end
|
||||||
|
|
||||||
|
Disclaimer
|
||||||
|
----------
|
||||||
|
|
||||||
|
.. include:: ../README.rst
|
||||||
|
:start-after: disclaimer-start
|
||||||
|
:end-before: disclaimer-end
|
||||||
|
|
||||||
..
|
..
|
||||||
* :ref:`genindex`
|
* :ref:`genindex`
|
||||||
* :ref:`modindex`
|
* :ref:`modindex`
|
||||||
|
|||||||
+10
-7
@@ -5,27 +5,27 @@ Install Instaloader
|
|||||||
|
|
||||||
.. highlight:: none
|
.. highlight:: none
|
||||||
|
|
||||||
**To install Instaloader**,
|
To **install Instaloader**,
|
||||||
|
|
||||||
#. Ensure that you have `Python <https://www.python.org/>`__, at least
|
#. Ensure that you have `Python <https://www.python.org/>`__, at least
|
||||||
version 3.5 and `pip <https://pypi.python.org/pypi/pip>`__
|
version 3.8, and `pip <https://pypi.python.org/pypi/pip>`__
|
||||||
installed
|
installed.
|
||||||
|
|
||||||
#. Then, install Instaloader using::
|
#. Then, install Instaloader using::
|
||||||
|
|
||||||
pip3 install instaloader
|
pip3 install instaloader
|
||||||
|
|
||||||
**To upgrade Instaloader**, do::
|
To **upgrade Instaloader** to its current version, do::
|
||||||
|
|
||||||
pip3 install --upgrade instaloader
|
pip3 install --upgrade instaloader
|
||||||
|
|
||||||
|
|
||||||
**Alternative methods for installing Instaloader:**
|
**Alternative methods** for installing Instaloader:
|
||||||
|
|
||||||
- If you do not want to use pip, even though it is highly recommended,
|
- If you do not want to use pip, even though it is highly recommended,
|
||||||
and prefer to **install Instaloader manually**,
|
and prefer to **install Instaloader manually**,
|
||||||
`Download the Source <https://github.com/instaloader/instaloader/releases/latest>`__,
|
`Download the Source <https://github.com/instaloader/instaloader/releases/latest>`__,
|
||||||
extract the Zip or Tarball and execute ``instaloader.py`` from there.
|
extract the Zip or Tarball and run ``setup.py`` from there.
|
||||||
|
|
||||||
- On **Arch Linux**, you may install Instaloader using the
|
- On **Arch Linux**, you may install Instaloader using the
|
||||||
`Instaloader AUR package <https://aur.archlinux.org/packages/instaloader/>`__.
|
`Instaloader AUR package <https://aur.archlinux.org/packages/instaloader/>`__.
|
||||||
@@ -33,6 +33,9 @@ Install Instaloader
|
|||||||
- On **Windows 10**, you may download the standalone executable from the
|
- On **Windows 10**, you may download the standalone executable from the
|
||||||
`current release page <https://github.com/instaloader/instaloader/releases/latest>`__.
|
`current release page <https://github.com/instaloader/instaloader/releases/latest>`__.
|
||||||
|
|
||||||
- To test the most current pre-release or development version of Instaloader::
|
- On **Android**, you can use Instaloader with `Termux <https://play.google.com/store/apps/details?id=com.termux>`__
|
||||||
|
after typing ``pkg install python`` and ``pip3 install instaloader``.
|
||||||
|
|
||||||
|
- To test the most current **pre-release** version of Instaloader::
|
||||||
|
|
||||||
pip3 install --pre instaloader
|
pip3 install --pre instaloader
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
Exceptions
|
||||||
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
.. module:: instaloader
|
||||||
|
:no-index:
|
||||||
|
|
||||||
|
.. 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:: LoginException
|
||||||
|
|
||||||
|
.. versionadded:: 4.12
|
||||||
|
|
||||||
|
.. autoexception:: TwoFactorAuthRequiredException
|
||||||
|
|
||||||
|
.. versionadded:: 4.2
|
||||||
|
|
||||||
|
.. versionchanged:: 4.12
|
||||||
|
Inherits LoginException
|
||||||
|
|
||||||
|
.. autoexception:: InvalidArgumentException
|
||||||
|
|
||||||
|
.. autoexception:: BadResponseException
|
||||||
|
|
||||||
|
.. autoexception:: BadCredentialsException
|
||||||
|
|
||||||
|
.. versionchanged:: 4.12
|
||||||
|
Inherits LoginException
|
||||||
|
|
||||||
|
.. autoexception:: PostChangedException
|
||||||
|
|
||||||
|
.. autoexception:: QueryReturnedNotFoundException
|
||||||
|
|
||||||
|
.. versionchanged:: 4.3
|
||||||
|
QueryReturnedNotFoundException now inherits ConnectionException
|
||||||
|
to retry on 404 errors.
|
||||||
|
|
||||||
|
|
||||||
|
.. autoexception:: TooManyRequestsException
|
||||||
|
|
||||||
|
.. autoexception:: AbortDownloadException
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
``Instaloader`` (Main Class)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. module:: instaloader
|
||||||
|
:no-index:
|
||||||
|
|
||||||
|
.. highlight:: python
|
||||||
|
|
||||||
|
.. autoclass:: Instaloader
|
||||||
|
:no-show-inheritance:
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
``InstaloaderContext`` (Low-level functions)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. module:: instaloader
|
||||||
|
:no-index:
|
||||||
|
|
||||||
|
.. highlight:: python
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:backlinks: none
|
||||||
|
|
||||||
|
``InstaloaderContext``
|
||||||
|
""""""""""""""""""""""
|
||||||
|
|
||||||
|
.. autoclass:: InstaloaderContext
|
||||||
|
:no-show-inheritance:
|
||||||
|
|
||||||
|
``RateController``
|
||||||
|
""""""""""""""""""
|
||||||
|
|
||||||
|
.. autoclass:: RateController
|
||||||
|
:no-show-inheritance:
|
||||||
|
|
||||||
|
.. versionadded:: 4.5
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
Resumable Iterations
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. module:: instaloader
|
||||||
|
:no-index:
|
||||||
|
|
||||||
|
.. 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:
|
||||||
|
|
||||||
|
A serializable representation of a :class:`NodeIterator` instance, saving
|
||||||
|
its iteration state.
|
||||||
|
|
||||||
|
It can be serialized and deserialized with :func:`save_structure_to_file`
|
||||||
|
and :func:`load_structure_from_file`, as well as with :mod:`json` and
|
||||||
|
:mod:`pickle` thanks to being a :class:`~typing.NamedTuple`.
|
||||||
|
|
||||||
|
``resumable_iteration``
|
||||||
|
"""""""""""""""""""""""
|
||||||
|
|
||||||
|
.. autofunction:: resumable_iteration
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
Instagram Structures
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
.. module:: instaloader
|
||||||
|
:no-index:
|
||||||
|
|
||||||
|
.. 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
|
||||||
|
|
||||||
|
TitlePic
|
||||||
|
""""""""
|
||||||
|
|
||||||
|
.. autoclass:: TitlePic
|
||||||
|
:no-show-inheritance:
|
||||||
|
|
||||||
|
.. versionadded:: 4.8
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
.. autofunction:: load_structure_from_file
|
||||||
|
|
||||||
|
.. autofunction:: get_json_structure
|
||||||
|
|
||||||
|
.. autofunction:: save_structure_to_file
|
||||||
|
|
||||||
|
LatestStamps
|
||||||
|
""""""""""""
|
||||||
|
|
||||||
|
.. autoclass:: LatestStamps
|
||||||
|
:no-show-inheritance:
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
requests
|
|
||||||
sphinx!=1.7.7
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
# 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,23 @@
|
|||||||
|
.. _sponsors:
|
||||||
|
|
||||||
|
Sponsoring the development of Instaloader
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
.. donations-start
|
||||||
|
|
||||||
|
.. include:: ../README.rst
|
||||||
|
:start-after: current-sponsors-start
|
||||||
|
:end-before: current-sponsors-end
|
||||||
|
|
||||||
|
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 GitHub Sponsors page, a PayPal.Me link and a Bitcoin address.
|
||||||
|
| GitHub Sponsors: `Sponsor @aandergr on GitHub Sponsors <https://github.com/sponsors/aandergr>`__
|
||||||
|
| PayPal: `PayPal.me/aandergr <https://www.paypal.me/aandergr>`__
|
||||||
|
| BTC: 1Nst4LoadeYzrKjJ1DX9CpbLXBYE9RKLwY
|
||||||
|
|
||||||
|
.. donations-end
|
||||||
+56
-24
@@ -5,32 +5,54 @@ Troubleshooting
|
|||||||
|
|
||||||
.. highlight:: python
|
.. highlight:: python
|
||||||
|
|
||||||
429 - Too Many Requests
|
429 Too Many Requests
|
||||||
-----------------------
|
---------------------
|
||||||
|
|
||||||
Instaloader has a logic to keep track of its requests to Instagram and to obey
|
Instaloader has a logic to keep track of its requests to Instagram and to obey
|
||||||
their rate limits. Since they are nowhere documented, we try them out
|
their rate limits. The rate controller assumes that
|
||||||
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.
|
- at one time, Instaloader is the only application that consumes requests, i.e.
|
||||||
neither the Instagram browser interface, nor a mobile app, nor another
|
neither the Instagram browser interface, nor a mobile app, nor another
|
||||||
Instaloader instance is running in parallel,
|
Instaloader instance is running in parallel, and
|
||||||
|
|
||||||
- no requests had been consumed when Instaloader starts.
|
- no requests had been consumed when Instaloader starts.
|
||||||
|
|
||||||
The latter one implies that restarting or reinstantiating Instaloader often
|
The latter one implies that restarting or reinstantiating Instaloader often
|
||||||
within short time is prone to cause a 429. When a request is denied with a 429,
|
within short time is prone to cause a 429.
|
||||||
|
|
||||||
|
Since the behavior of the rate controller might change between different
|
||||||
|
versions of Instaloader, make sure to use the current version of Instaloader,
|
||||||
|
especially when encountering many 429 errors.
|
||||||
|
|
||||||
|
If a request is denied with a 429,
|
||||||
Instaloader retries the request as soon as the temporary ban is assumed to be
|
Instaloader retries the request as soon as the temporary ban is assumed to be
|
||||||
expired. In case the retry continuously fails for some reason, which should not
|
expired. In case the retry continuously fails for some reason, which should not
|
||||||
happen in normal conditions, consider adjusting the
|
happen under normal conditions, consider adjusting the
|
||||||
:option:`--max-connection-attempts` option.
|
:option:`--max-connection-attempts` option.
|
||||||
|
|
||||||
|
There have been observations that services, that in their nature offer
|
||||||
|
promiscuous IP addresses, such as cloud, VPN and public proxy services, might be
|
||||||
|
subject to significantly stricter limits for anonymous access. However,
|
||||||
|
:ref:`logged-in accesses<login>` do not seem to be affected.
|
||||||
|
|
||||||
|
Instaloader allows to adjust the rate controlling behavior by overriding
|
||||||
|
:class:`instaloader.RateController`.
|
||||||
|
|
||||||
|
Too many queries in the last time
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
**"Too many queries in the last time"** is not an error. It is a notice that the
|
**"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
|
rate limit has almost been reached, according to Instaloader's own rate
|
||||||
accounting mechanism. We regularly adjust this mechanism to match Instagram's
|
accounting mechanism.
|
||||||
current rate limiting.
|
|
||||||
|
Instaloader allows to adjust the rate controlling behavior by overriding
|
||||||
|
:class:`instaloader.RateController`.
|
||||||
|
|
||||||
|
Private but not followed
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
You have to follow a private account to access most of its associated
|
||||||
|
information.
|
||||||
|
|
||||||
Login error
|
Login error
|
||||||
-----------
|
-----------
|
||||||
@@ -40,21 +62,31 @@ Two-Factor-Authentication. It also supports handling the *checkpoint challenge*,
|
|||||||
issued when Instagram suspects authentication activity on your account, by
|
issued when Instagram suspects authentication activity on your account, by
|
||||||
pointing the user to an URL to be opened in a browser.
|
pointing the user to an URL to be opened in a browser.
|
||||||
|
|
||||||
Nevertheless, in :issue:`92` users report problems with logging in. To still use
|
Nevertheless, in :issue:`92`, :issue:`615`, :issue:`1150` and :issue:`1217`,
|
||||||
Instaloader's logged-in functionality, you may use the following script to
|
users reported problems with
|
||||||
workaround login problems by importing the session cookies from Firefox and
|
logging in. We recommend to always keep the session file which Instaloader
|
||||||
bypassing Instaloader's login.
|
creates when using :option:`--login`. If a session file is present,
|
||||||
|
:option:`--login` does not make use of the failure-prone login procedure.
|
||||||
|
Also, session files usually do not expire.
|
||||||
|
|
||||||
.. literalinclude:: codesnippets/92_import_firefox_session.py
|
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.
|
||||||
|
|
||||||
To use this,
|
.. literalinclude:: codesnippets/615_import_firefox_session.py
|
||||||
|
|
||||||
#. login to Instagram in Firefox,
|
To use this script,
|
||||||
|
|
||||||
#. execute the snippet,
|
#. Download the script: :example:`615_import_firefox_session.py`,
|
||||||
|
|
||||||
#. then, ``instaloader -l USERNAME`` should work fine.
|
#. Login to Instagram in Firefox,
|
||||||
|
|
||||||
If you do not use your default firefox profile, or your operating system has the
|
#. Execute the snippet, e.g. with ``python 615_import_firefox_session.py``,
|
||||||
paths differently set up, you may have to alter the ``FIREFOXCOOKIEFILE``
|
|
||||||
variable first.
|
#. 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.
|
||||||
|
|||||||
+24
-5
@@ -1,7 +1,7 @@
|
|||||||
"""Download pictures (or videos) along with their captions and other metadata from Instagram."""
|
"""Download pictures (or videos) along with their captions and other metadata from Instagram."""
|
||||||
|
|
||||||
|
|
||||||
__version__ = '4.3a2'
|
__version__ = '4.15.4'
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -13,7 +13,26 @@ else:
|
|||||||
win_unicode_console.enable()
|
win_unicode_console.enable()
|
||||||
|
|
||||||
from .exceptions import *
|
from .exceptions import *
|
||||||
from .instaloader import Instaloader
|
from .instaloader import Instaloader as Instaloader
|
||||||
from .instaloadercontext import InstaloaderContext
|
from .instaloadercontext import (InstaloaderContext as InstaloaderContext,
|
||||||
from .structures import (Highlight, Post, PostSidecarNode, PostComment, PostCommentAnswer, PostLocation, Profile, Story,
|
RateController as RateController)
|
||||||
StoryItem, TopSearchResults, load_structure_from_file, save_structure_to_file)
|
from .lateststamps import LatestStamps as LatestStamps
|
||||||
|
from .nodeiterator import (NodeIterator as NodeIterator,
|
||||||
|
FrozenNodeIterator as FrozenNodeIterator,
|
||||||
|
resumable_iteration as resumable_iteration)
|
||||||
|
from .structures import (Hashtag as Hashtag,
|
||||||
|
Highlight as Highlight,
|
||||||
|
Post as Post,
|
||||||
|
PostSidecarNode as PostSidecarNode,
|
||||||
|
PostComment as PostComment,
|
||||||
|
PostCommentAnswer as PostCommentAnswer,
|
||||||
|
PostLocation as PostLocation,
|
||||||
|
Profile as Profile,
|
||||||
|
Story as Story,
|
||||||
|
StoryItem as StoryItem,
|
||||||
|
TopSearchResults as TopSearchResults,
|
||||||
|
TitlePic as TitlePic,
|
||||||
|
load_structure_from_file as load_structure_from_file,
|
||||||
|
save_structure_to_file as save_structure_to_file,
|
||||||
|
load_structure as load_structure,
|
||||||
|
get_json_structure as get_json_structure)
|
||||||
|
|||||||
+272
-80
@@ -3,28 +3,54 @@
|
|||||||
import ast
|
import ast
|
||||||
import datetime
|
import datetime
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from argparse import ArgumentParser, SUPPRESS
|
from argparse import ArgumentParser, ArgumentTypeError, SUPPRESS
|
||||||
|
from enum import IntEnum
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
from . import (Instaloader, InstaloaderException, InvalidArgumentException, Post, Profile, ProfileNotExistsException,
|
from . import (AbortDownloadException, BadCredentialsException, Instaloader, InstaloaderException,
|
||||||
StoryItem, __version__, load_structure_from_file, TwoFactorAuthRequiredException,
|
InvalidArgumentException, LoginException, Post, Profile, ProfileNotExistsException, StoryItem,
|
||||||
BadCredentialsException)
|
TwoFactorAuthRequiredException, __version__, load_structure_from_file)
|
||||||
from .instaloader import get_default_session_filename
|
from .instaloader import (get_default_session_filename, get_default_stamps_filename)
|
||||||
from .instaloadercontext import default_user_agent
|
from .instaloadercontext import default_user_agent
|
||||||
|
from .lateststamps import LatestStamps
|
||||||
|
try:
|
||||||
|
import browser_cookie3
|
||||||
|
bc3_library = True
|
||||||
|
except ImportError:
|
||||||
|
bc3_library = False
|
||||||
|
|
||||||
|
|
||||||
|
class ExitCode(IntEnum):
|
||||||
|
SUCCESS = 0
|
||||||
|
NON_FATAL_ERROR = 1
|
||||||
|
INIT_FAILURE = 2
|
||||||
|
LOGIN_FAILURE = 3
|
||||||
|
DOWNLOAD_ABORTED = 4
|
||||||
|
USER_ABORTED = 5
|
||||||
|
UNEXPECTED_ERROR = 99
|
||||||
|
|
||||||
def usage_string():
|
def usage_string():
|
||||||
# NOTE: duplicated in README.rst and docs/index.rst
|
# NOTE: duplicated in README.rst and docs/index.rst
|
||||||
argv0 = os.path.basename(sys.argv[0])
|
argv0 = os.path.basename(sys.argv[0])
|
||||||
argv0 = "instaloader" if argv0 == "__main__.py" else argv0
|
argv0 = "instaloader" if argv0 == "__main__.py" else argv0
|
||||||
return """
|
return """
|
||||||
{0} [--comments] [--geotags] [--stories] [--highlights] [--tagged]
|
{0} [--comments] [--geotags]
|
||||||
|
{2:{1}} [--stories] [--highlights] [--tagged] [--reels] [--igtv]
|
||||||
{2:{1}} [--login YOUR-USERNAME] [--fast-update]
|
{2:{1}} [--login YOUR-USERNAME] [--fast-update]
|
||||||
{2:{1}} profile | "#hashtag" | %%location_id | :stories | :feed | :saved
|
{2:{1}} profile | "#hashtag" | %%location_id | :stories | :feed | :saved
|
||||||
{0} --help""".format(argv0, len(argv0), '')
|
{0} --help""".format(argv0, len(argv0), '')
|
||||||
|
|
||||||
|
|
||||||
|
def http_status_code_list(code_list_str: str) -> List[int]:
|
||||||
|
codes = [int(s) for s in code_list_str.split(',')]
|
||||||
|
for code in codes:
|
||||||
|
if not 100 <= code <= 599:
|
||||||
|
raise ArgumentTypeError("Invalid HTTP status code: {}".format(code))
|
||||||
|
return codes
|
||||||
|
|
||||||
|
|
||||||
def filterstr_to_filterfunc(filter_str: str, item_type: type):
|
def filterstr_to_filterfunc(filter_str: str, item_type: type):
|
||||||
"""Takes an --post-filter=... or --storyitem-filter=... filter
|
"""Takes an --post-filter=... or --storyitem-filter=... filter
|
||||||
specification and makes a filter_func Callable out of it."""
|
specification and makes a filter_func Callable out of it."""
|
||||||
@@ -34,7 +60,6 @@ def filterstr_to_filterfunc(filter_str: str, item_type: type):
|
|||||||
|
|
||||||
class TransformFilterAst(ast.NodeTransformer):
|
class TransformFilterAst(ast.NodeTransformer):
|
||||||
def visit_Name(self, node: ast.Name):
|
def visit_Name(self, node: ast.Name):
|
||||||
# pylint:disable=no-self-use
|
|
||||||
if not isinstance(node.ctx, ast.Load):
|
if not isinstance(node.ctx, ast.Load):
|
||||||
raise InvalidArgumentException("Invalid filter: Modifying variables ({}) not allowed.".format(node.id))
|
raise InvalidArgumentException("Invalid filter: Modifying variables ({}) not allowed.".format(node.id))
|
||||||
if node.id == "datetime":
|
if node.id == "datetime":
|
||||||
@@ -57,14 +82,68 @@ def filterstr_to_filterfunc(filter_str: str, item_type: type):
|
|||||||
return filterfunc
|
return filterfunc
|
||||||
|
|
||||||
|
|
||||||
|
def get_cookies_from_instagram(domain: str, browser: str, cookie_file: Optional[str] = None):
|
||||||
|
supported_browsers = {
|
||||||
|
"brave": browser_cookie3.brave,
|
||||||
|
"chrome": browser_cookie3.chrome,
|
||||||
|
"chromium": browser_cookie3.chromium,
|
||||||
|
"edge": browser_cookie3.edge,
|
||||||
|
"firefox": browser_cookie3.firefox,
|
||||||
|
"librewolf": browser_cookie3.librewolf,
|
||||||
|
"opera": browser_cookie3.opera,
|
||||||
|
"opera_gx": browser_cookie3.opera_gx,
|
||||||
|
"safari": browser_cookie3.safari,
|
||||||
|
"vivaldi": browser_cookie3.vivaldi,
|
||||||
|
}
|
||||||
|
|
||||||
|
if browser not in supported_browsers:
|
||||||
|
raise InvalidArgumentException("Loading cookies from the specified browser failed\n"
|
||||||
|
"Supported browsers are Brave, Chrome, Chromium, Edge, Firefox, LibreWolf, "
|
||||||
|
"Opera, Opera_GX, Safari and Vivaldi")
|
||||||
|
|
||||||
|
cookies = {}
|
||||||
|
browser_cookies = list(supported_browsers[browser](cookie_file=cookie_file))
|
||||||
|
|
||||||
|
for cookie in browser_cookies:
|
||||||
|
if domain in cookie.domain:
|
||||||
|
cookies[cookie.name] = cookie.value
|
||||||
|
|
||||||
|
if cookies:
|
||||||
|
print(f"Cookies loaded successfully from {browser}")
|
||||||
|
else:
|
||||||
|
raise LoginException(f"No cookies found for Instagram in {browser}, "
|
||||||
|
f"Are you logged in successfully in {browser}?")
|
||||||
|
|
||||||
|
return cookies
|
||||||
|
|
||||||
|
|
||||||
|
def import_session(browser: str, instaloader: Instaloader, cookiefile: Optional[str]):
|
||||||
|
cookie = get_cookies_from_instagram('instagram', browser, cookiefile)
|
||||||
|
if cookie is not None:
|
||||||
|
instaloader.context.update_cookies(cookie)
|
||||||
|
username = instaloader.test_login()
|
||||||
|
if not username:
|
||||||
|
raise LoginException(f"Not logged in. Are you logged in successfully in {browser}?")
|
||||||
|
instaloader.context.username = username
|
||||||
|
print(f"{username} has been successfully logged in.")
|
||||||
|
print(f"Next time use --login={username} to reuse the same session.")
|
||||||
|
|
||||||
|
|
||||||
def _main(instaloader: Instaloader, targetlist: List[str],
|
def _main(instaloader: Instaloader, targetlist: List[str],
|
||||||
username: Optional[str] = None, password: Optional[str] = None,
|
username: Optional[str] = None, password: Optional[str] = None,
|
||||||
sessionfile: Optional[str] = None,
|
sessionfile: Optional[str] = None,
|
||||||
download_profile_pic: bool = True, download_posts=True,
|
download_profile_pic: bool = True, download_posts=True,
|
||||||
download_stories: bool = False, download_highlights: bool = False, download_tagged: bool = False,
|
download_stories: bool = False,
|
||||||
|
download_highlights: bool = False,
|
||||||
|
download_tagged: bool = False,
|
||||||
|
download_reels: bool = False,
|
||||||
|
download_igtv: bool = False,
|
||||||
fast_update: bool = False,
|
fast_update: bool = False,
|
||||||
|
latest_stamps_file: Optional[str] = None,
|
||||||
max_count: Optional[int] = None, post_filter_str: Optional[str] = None,
|
max_count: Optional[int] = None, post_filter_str: Optional[str] = None,
|
||||||
storyitem_filter_str: Optional[str] = None) -> None:
|
storyitem_filter_str: Optional[str] = None,
|
||||||
|
browser: Optional[str] = None,
|
||||||
|
cookiefile: Optional[str] = None) -> ExitCode:
|
||||||
"""Download set of profiles, hashtags etc. and handle logging in and session files if desired."""
|
"""Download set of profiles, hashtags etc. and handle logging in and session files if desired."""
|
||||||
# Parse and generate filter function
|
# Parse and generate filter function
|
||||||
post_filter = None
|
post_filter = None
|
||||||
@@ -75,8 +154,19 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
|||||||
if storyitem_filter_str is not None:
|
if storyitem_filter_str is not None:
|
||||||
storyitem_filter = filterstr_to_filterfunc(storyitem_filter_str, StoryItem)
|
storyitem_filter = filterstr_to_filterfunc(storyitem_filter_str, StoryItem)
|
||||||
instaloader.context.log('Only download storyitems with property "{}".'.format(storyitem_filter_str))
|
instaloader.context.log('Only download storyitems with property "{}".'.format(storyitem_filter_str))
|
||||||
|
latest_stamps = None
|
||||||
|
if latest_stamps_file is not None:
|
||||||
|
latest_stamps = LatestStamps(latest_stamps_file)
|
||||||
|
instaloader.context.log(f"Using latest stamps from {latest_stamps_file}.")
|
||||||
|
# load cookies if browser is not None
|
||||||
|
if browser and bc3_library:
|
||||||
|
import_session(browser.lower(), instaloader, cookiefile)
|
||||||
|
elif browser and not bc3_library:
|
||||||
|
raise InvalidArgumentException("browser_cookie3 library is needed to load cookies from browsers")
|
||||||
# Login, if desired
|
# Login, if desired
|
||||||
if username is not None:
|
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:
|
try:
|
||||||
instaloader.load_session_from_file(username, sessionfile)
|
instaloader.load_session_from_file(username, sessionfile)
|
||||||
except FileNotFoundError as err:
|
except FileNotFoundError as err:
|
||||||
@@ -88,22 +178,32 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
|||||||
try:
|
try:
|
||||||
instaloader.login(username, password)
|
instaloader.login(username, password)
|
||||||
except TwoFactorAuthRequiredException:
|
except TwoFactorAuthRequiredException:
|
||||||
|
# https://github.com/instaloader/instaloader/issues/1217
|
||||||
|
instaloader.context.error("Warning: There have been reports of 2FA currently not working. "
|
||||||
|
"Consider importing session cookies from your browser with "
|
||||||
|
"--load-cookies.")
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
code = input("Enter 2FA verification code: ")
|
code = input("Enter 2FA verification code: ")
|
||||||
instaloader.two_factor_login(code)
|
instaloader.two_factor_login(code)
|
||||||
break
|
break
|
||||||
except BadCredentialsException:
|
except BadCredentialsException as err:
|
||||||
|
print(err, file=sys.stderr)
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
instaloader.interactive_login(username)
|
try:
|
||||||
|
instaloader.interactive_login(username)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nInterrupted by user.", file=sys.stderr)
|
||||||
|
return ExitCode.USER_ABORTED
|
||||||
instaloader.context.log("Logged in as %s." % username)
|
instaloader.context.log("Logged in as %s." % username)
|
||||||
# since 4.2.9 login is required for geotags
|
# since 4.2.9 login is required for geotags
|
||||||
if instaloader.download_geotags and not instaloader.context.is_logged_in:
|
if instaloader.download_geotags and not instaloader.context.is_logged_in:
|
||||||
instaloader.context.error("Warning: Use --login to download geotags of posts.")
|
instaloader.context.error("Warning: Login is required to download geotags of posts.")
|
||||||
# Try block for KeyboardInterrupt (save session on ^C)
|
# Try block for KeyboardInterrupt (save session on ^C)
|
||||||
profiles = set()
|
profiles = set()
|
||||||
anonymous_retry_profiles = set()
|
anonymous_retry_profiles = set()
|
||||||
|
exit_code = ExitCode.SUCCESS
|
||||||
try:
|
try:
|
||||||
# Generate set of profiles, already downloading non-profile targets
|
# Generate set of profiles, already downloading non-profile targets
|
||||||
for target in targetlist:
|
for target in targetlist:
|
||||||
@@ -132,18 +232,19 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
|||||||
# strip '/' characters to be more shell-autocompletion-friendly
|
# strip '/' characters to be more shell-autocompletion-friendly
|
||||||
target = target.rstrip('/')
|
target = target.rstrip('/')
|
||||||
with instaloader.context.error_catcher(target):
|
with instaloader.context.error_catcher(target):
|
||||||
if target[0] == '@':
|
if re.match(r"^@[A-Za-z0-9._]+$", target):
|
||||||
instaloader.context.log("Retrieving followees of %s..." % target[1:])
|
instaloader.context.log("Retrieving followees of %s..." % target[1:])
|
||||||
profile = Profile.from_username(instaloader.context, target[1:])
|
profile = Profile.from_username(instaloader.context, target[1:])
|
||||||
for followee in profile.get_followees():
|
for followee in profile.get_followees():
|
||||||
instaloader.save_profile_id(followee)
|
instaloader.save_profile_id(followee)
|
||||||
profiles.add(followee)
|
profiles.add(followee)
|
||||||
elif target[0] == '#':
|
elif re.match(r"^#\w+$", target):
|
||||||
instaloader.download_hashtag(hashtag=target[1:], max_count=max_count, fast_update=fast_update,
|
instaloader.download_hashtag(hashtag=target[1:], max_count=max_count, fast_update=fast_update,
|
||||||
post_filter=post_filter)
|
post_filter=post_filter,
|
||||||
elif target[0] == '-':
|
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)
|
instaloader.download_post(Post.from_shortcode(instaloader.context, target[1:]), target)
|
||||||
elif target[0] == "%":
|
elif re.match(r"^%[0-9]+$", target):
|
||||||
instaloader.download_location(location=target[1:], max_count=max_count, fast_update=fast_update,
|
instaloader.download_location(location=target[1:], max_count=max_count, fast_update=fast_update,
|
||||||
post_filter=post_filter)
|
post_filter=post_filter)
|
||||||
elif target == ":feed":
|
elif target == ":feed":
|
||||||
@@ -154,11 +255,14 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
|||||||
elif target == ":saved":
|
elif target == ":saved":
|
||||||
instaloader.download_saved_posts(fast_update=fast_update, max_count=max_count,
|
instaloader.download_saved_posts(fast_update=fast_update, max_count=max_count,
|
||||||
post_filter=post_filter)
|
post_filter=post_filter)
|
||||||
else:
|
elif re.match(r"^[A-Za-z0-9._]+$", target):
|
||||||
|
download_profile_content = download_posts or download_tagged or download_reels or download_igtv
|
||||||
try:
|
try:
|
||||||
profile = instaloader.check_profile_id(target)
|
profile = instaloader.check_profile_id(target, latest_stamps)
|
||||||
if instaloader.context.is_logged_in and profile.has_blocked_viewer:
|
if instaloader.context.is_logged_in and profile.has_blocked_viewer:
|
||||||
if download_profile_pic or ((download_posts or download_tagged) and not profile.is_private):
|
if download_profile_pic or (
|
||||||
|
download_profile_content and not profile.is_private
|
||||||
|
):
|
||||||
raise ProfileNotExistsException("{} blocked you; But we download her anonymously."
|
raise ProfileNotExistsException("{} blocked you; But we download her anonymously."
|
||||||
.format(target))
|
.format(target))
|
||||||
else:
|
else:
|
||||||
@@ -168,34 +272,65 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
|||||||
except ProfileNotExistsException as err:
|
except ProfileNotExistsException as err:
|
||||||
# Not only our profile.has_blocked_viewer condition raises ProfileNotExistsException,
|
# 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.
|
# 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
|
if instaloader.context.is_logged_in and (download_profile_pic or download_profile_content):
|
||||||
download_tagged):
|
|
||||||
instaloader.context.log(err)
|
instaloader.context.log(err)
|
||||||
instaloader.context.log("Trying again anonymously, helps in case you are just blocked.")
|
instaloader.context.log("Trying again anonymously, helps in case you are just blocked.")
|
||||||
with instaloader.anonymous_copy() as anonymous_loader:
|
with instaloader.anonymous_copy() as anonymous_loader:
|
||||||
with instaloader.context.error_catcher():
|
with instaloader.context.error_catcher():
|
||||||
anonymous_retry_profiles.add(anonymous_loader.check_profile_id(target))
|
anonymous_retry_profiles.add(anonymous_loader.check_profile_id(target,
|
||||||
|
latest_stamps))
|
||||||
instaloader.context.error("Warning: {} will be downloaded anonymously (\"{}\")."
|
instaloader.context.error("Warning: {} will be downloaded anonymously (\"{}\")."
|
||||||
.format(target, err))
|
.format(target, err))
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
else:
|
||||||
|
target_type = {
|
||||||
|
'#': 'hashtag',
|
||||||
|
'%': 'location',
|
||||||
|
'-': 'shortcode',
|
||||||
|
}.get(target[0], 'username')
|
||||||
|
raise ProfileNotExistsException('Invalid {} {}'.format(target_type, target))
|
||||||
if len(profiles) > 1:
|
if len(profiles) > 1:
|
||||||
instaloader.context.log("Downloading {} profiles: {}".format(len(profiles),
|
instaloader.context.log("Downloading {} profiles: {}".format(len(profiles),
|
||||||
' '.join([p.username for p in profiles])))
|
' '.join([p.username for p in profiles])))
|
||||||
if profiles and download_profile_pic and not instaloader.context.is_logged_in:
|
if instaloader.context.iphone_support and profiles and (download_profile_pic or download_posts) and \
|
||||||
instaloader.context.error("Warning: Use --login to download HD version of profile pictures.")
|
not instaloader.context.is_logged_in:
|
||||||
instaloader.download_profiles(profiles,
|
instaloader.context.log("Hint: Login to download higher-quality versions of pictures.")
|
||||||
download_profile_pic, download_posts, download_tagged, download_highlights,
|
instaloader.download_profiles(
|
||||||
download_stories, fast_update, post_filter, storyitem_filter)
|
profiles,
|
||||||
|
download_profile_pic,
|
||||||
|
download_posts,
|
||||||
|
download_tagged,
|
||||||
|
download_igtv,
|
||||||
|
download_highlights,
|
||||||
|
download_stories,
|
||||||
|
fast_update,
|
||||||
|
post_filter,
|
||||||
|
storyitem_filter,
|
||||||
|
latest_stamps=latest_stamps,
|
||||||
|
reels=download_reels,
|
||||||
|
)
|
||||||
if anonymous_retry_profiles:
|
if anonymous_retry_profiles:
|
||||||
instaloader.context.log("Downloading anonymously: {}"
|
instaloader.context.log("Downloading anonymously: {}"
|
||||||
.format(' '.join([p.username for p in anonymous_retry_profiles])))
|
.format(' '.join([p.username for p in anonymous_retry_profiles])))
|
||||||
with instaloader.anonymous_copy() as anonymous_loader:
|
with instaloader.anonymous_copy() as anonymous_loader:
|
||||||
anonymous_loader.download_profiles(anonymous_retry_profiles,
|
anonymous_loader.download_profiles(
|
||||||
download_profile_pic, download_posts, download_tagged,
|
anonymous_retry_profiles,
|
||||||
fast_update=fast_update, post_filter=post_filter)
|
download_profile_pic,
|
||||||
|
download_posts,
|
||||||
|
download_tagged,
|
||||||
|
download_igtv,
|
||||||
|
fast_update=fast_update,
|
||||||
|
post_filter=post_filter,
|
||||||
|
latest_stamps=latest_stamps,
|
||||||
|
reels=download_reels
|
||||||
|
)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nInterrupted by user.", file=sys.stderr)
|
print("\nInterrupted by user.", file=sys.stderr)
|
||||||
|
exit_code = ExitCode.USER_ABORTED
|
||||||
|
except AbortDownloadException as exc:
|
||||||
|
print("\nDownload aborted: {}.".format(exc), file=sys.stderr)
|
||||||
|
exit_code = ExitCode.DOWNLOAD_ABORTED
|
||||||
# Save session if it is useful
|
# Save session if it is useful
|
||||||
if instaloader.context.is_logged_in:
|
if instaloader.context.is_logged_in:
|
||||||
instaloader.save_session_to_file(sessionfile)
|
instaloader.save_session_to_file(sessionfile)
|
||||||
@@ -205,14 +340,15 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
|||||||
# Instaloader did at least save a session file
|
# Instaloader did at least save a session file
|
||||||
instaloader.context.log("No targets were specified, thus nothing has been downloaded.")
|
instaloader.context.log("No targets were specified, thus nothing has been downloaded.")
|
||||||
else:
|
else:
|
||||||
# Instloader did not do anything
|
# Instaloader did not do anything
|
||||||
instaloader.context.log("usage:" + usage_string())
|
instaloader.context.log("usage:" + usage_string())
|
||||||
|
exit_code = ExitCode.INIT_FAILURE
|
||||||
|
return exit_code
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = ArgumentParser(description=__doc__, add_help=False, usage=usage_string(),
|
parser = ArgumentParser(description=__doc__, add_help=False, usage=usage_string(),
|
||||||
epilog="Report issues at https://github.com/instaloader/instaloader/issues. "
|
epilog="The complete documentation can be found at "
|
||||||
"The complete documentation can be found at "
|
|
||||||
"https://instaloader.github.io/.",
|
"https://instaloader.github.io/.",
|
||||||
fromfile_prefix_chars='+')
|
fromfile_prefix_chars='+')
|
||||||
|
|
||||||
@@ -223,17 +359,17 @@ def main():
|
|||||||
help="Download profile. If an already-downloaded profile has been renamed, Instaloader "
|
help="Download profile. If an already-downloaded profile has been renamed, Instaloader "
|
||||||
"automatically finds it by its unique ID and renames the folder likewise.")
|
"automatically finds it by its unique ID and renames the folder likewise.")
|
||||||
g_targets.add_argument('_at_profile', nargs='*', metavar="@profile",
|
g_targets.add_argument('_at_profile', nargs='*', metavar="@profile",
|
||||||
help="Download all followees of profile. Requires --login. "
|
help="Download all followees of profile. Requires login. "
|
||||||
"Consider using :feed rather than @yourself.")
|
"Consider using :feed rather than @yourself.")
|
||||||
g_targets.add_argument('_hashtag', nargs='*', metavar='"#hashtag"', help="Download #hashtag.")
|
g_targets.add_argument('_hashtag', nargs='*', metavar='"#hashtag"', help="Download #hashtag.")
|
||||||
g_targets.add_argument('_location', nargs='*', metavar='%location_id',
|
g_targets.add_argument('_location', nargs='*', metavar='%location_id',
|
||||||
help="Download %%location_id. Requires --login.")
|
help="Download %%location_id. Requires login.")
|
||||||
g_targets.add_argument('_feed', nargs='*', metavar=":feed",
|
g_targets.add_argument('_feed', nargs='*', metavar=":feed",
|
||||||
help="Download pictures from your feed. Requires --login.")
|
help="Download pictures from your feed. Requires login.")
|
||||||
g_targets.add_argument('_stories', nargs='*', metavar=":stories",
|
g_targets.add_argument('_stories', nargs='*', metavar=":stories",
|
||||||
help="Download the stories of your followees. Requires --login.")
|
help="Download the stories of your followees. Requires login.")
|
||||||
g_targets.add_argument('_saved', nargs='*', metavar=":saved",
|
g_targets.add_argument('_saved', nargs='*', metavar=":saved",
|
||||||
help="Download the posts that you marked as saved. Requires --login.")
|
help="Download the posts that you marked as saved. Requires login.")
|
||||||
g_targets.add_argument('_singlepost', nargs='*', metavar="-- -shortcode",
|
g_targets.add_argument('_singlepost', nargs='*', metavar="-- -shortcode",
|
||||||
help="Download the post with the given shortcode")
|
help="Download the post with the given shortcode")
|
||||||
g_targets.add_argument('_json', nargs='*', metavar="filename.json[.xz]",
|
g_targets.add_argument('_json', nargs='*', metavar="filename.json[.xz]",
|
||||||
@@ -251,6 +387,8 @@ def main():
|
|||||||
help="Do not download regular posts.")
|
help="Do not download regular posts.")
|
||||||
g_prof.add_argument('--no-profile-pic', action='store_true',
|
g_prof.add_argument('--no-profile-pic', action='store_true',
|
||||||
help='Do not download profile picture.')
|
help='Do not download profile picture.')
|
||||||
|
g_post.add_argument('--slide', action='store',
|
||||||
|
help='Set what image/interval of a sidecar you want to download.')
|
||||||
g_post.add_argument('--no-pictures', action='store_true',
|
g_post.add_argument('--no-pictures', action='store_true',
|
||||||
help='Do not download post pictures. Cannot be used together with --fast-update. '
|
help='Do not download post pictures. Cannot be used together with --fast-update. '
|
||||||
'Implies --no-video-thumbnails, does not imply --no-videos.')
|
'Implies --no-video-thumbnails, does not imply --no-videos.')
|
||||||
@@ -262,11 +400,11 @@ def main():
|
|||||||
help='Download geotags when available. Geotags are stored as a '
|
help='Download geotags when available. Geotags are stored as a '
|
||||||
'text file with the location\'s name and a Google Maps link. '
|
'text file with the location\'s name and a Google Maps link. '
|
||||||
'This requires an additional request to the Instagram '
|
'This requires an additional request to the Instagram '
|
||||||
'server for each picture. Requires --login.')
|
'server for each picture. Requires login.')
|
||||||
g_post.add_argument('-C', '--comments', action='store_true',
|
g_post.add_argument('-C', '--comments', action='store_true',
|
||||||
help='Download and update comments for each post. '
|
help='Download and update comments for each post. '
|
||||||
'This requires an additional request to the Instagram '
|
'This requires an additional request to the Instagram '
|
||||||
'server for each post, which is why it is disabled by default.')
|
'server for each post, which is why it is disabled by default. Requires login.')
|
||||||
g_post.add_argument('--no-captions', action='store_true',
|
g_post.add_argument('--no-captions', action='store_true',
|
||||||
help='Do not create txt files.')
|
help='Do not create txt files.')
|
||||||
g_post.add_argument('--post-metadata-txt', action='append',
|
g_post.add_argument('--post-metadata-txt', action='append',
|
||||||
@@ -280,20 +418,27 @@ def main():
|
|||||||
g_post.add_argument('--no-compress-json', action='store_true',
|
g_post.add_argument('--no-compress-json', action='store_true',
|
||||||
help='Do not xz compress JSON files, rather create pretty formatted JSONs.')
|
help='Do not xz compress JSON files, rather create pretty formatted JSONs.')
|
||||||
g_prof.add_argument('-s', '--stories', action='store_true',
|
g_prof.add_argument('-s', '--stories', action='store_true',
|
||||||
help='Also download stories of each profile that is downloaded. Requires --login.')
|
help='Also download stories of each profile that is downloaded. Requires login.')
|
||||||
g_prof.add_argument('--stories-only', action='store_true',
|
g_prof.add_argument('--stories-only', action='store_true',
|
||||||
help=SUPPRESS)
|
help=SUPPRESS)
|
||||||
g_prof.add_argument('--highlights', action='store_true',
|
g_prof.add_argument('--highlights', action='store_true',
|
||||||
help='Also download highlights of each profile that is downloaded. Requires --login.')
|
help='Also download highlights of each profile that is downloaded. Requires login.')
|
||||||
g_prof.add_argument('--tagged', action='store_true',
|
g_prof.add_argument('--tagged', action='store_true',
|
||||||
help='Also download posts where each profile is tagged.')
|
help='Also download posts where each profile is tagged.')
|
||||||
|
g_prof.add_argument('--reels', action='store_true',
|
||||||
|
help='Also download Reels videos.')
|
||||||
|
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 = parser.add_argument_group("Which Posts to Download")
|
||||||
|
|
||||||
g_cond.add_argument('-F', '--fast-update', action='store_true',
|
g_cond.add_argument('-F', '--fast-update', action='store_true',
|
||||||
help='For each target, stop when encountering the first already-downloaded picture. This '
|
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.')
|
'flag is recommended when you use Instaloader to update your personal Instagram archive.')
|
||||||
|
g_cond.add_argument('--latest-stamps', nargs='?', metavar='STAMPSFILE', const=get_default_stamps_filename(),
|
||||||
|
help='Store the timestamps of latest media scraped for each profile. This allows updating '
|
||||||
|
'your personal Instagram archive even if you delete the destination directories. '
|
||||||
|
'If STAMPSFILE is not provided, defaults to ' + get_default_stamps_filename())
|
||||||
g_cond.add_argument('--post-filter', '--only-if', metavar='filter',
|
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 '
|
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 '
|
'a syntactically valid python expression. Variables are evaluated to '
|
||||||
@@ -305,15 +450,20 @@ def main():
|
|||||||
|
|
||||||
g_cond.add_argument('-c', '--count',
|
g_cond.add_argument('-c', '--count',
|
||||||
help='Do not attempt to download more than COUNT posts. '
|
help='Do not attempt to download more than COUNT posts. '
|
||||||
'Applies only to #hashtag and :feed.')
|
'Applies to #hashtag, %%location_id, :feed, and :saved.')
|
||||||
|
|
||||||
g_login = parser.add_argument_group('Login (Download Private Profiles)',
|
g_login = parser.add_argument_group('Login (Download Private Profiles)',
|
||||||
'Instaloader can login to Instagram. This allows downloading private profiles. '
|
'Instaloader can login to Instagram. This allows downloading private profiles. '
|
||||||
'To login, pass the --login option. Your session cookie (not your password!) '
|
'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 '
|
'will be saved to a local file to be reused next time you want Instaloader '
|
||||||
'to login.')
|
'to login. Instead of --login, the --load-cookies option can be used to '
|
||||||
|
'import a session from a browser.')
|
||||||
g_login.add_argument('-l', '--login', metavar='YOUR-USERNAME',
|
g_login.add_argument('-l', '--login', metavar='YOUR-USERNAME',
|
||||||
help='Login name (profile name) for your Instagram account.')
|
help='Login name (profile name) for your Instagram account.')
|
||||||
|
g_login.add_argument('-b', '--load-cookies', metavar='BROWSER-NAME',
|
||||||
|
help='Browser name to load cookies from Instagram')
|
||||||
|
g_login.add_argument('-B', '--cookiefile', metavar='COOKIE-FILE',
|
||||||
|
help='Cookie file of a profile to load cookies')
|
||||||
g_login.add_argument('-f', '--sessionfile',
|
g_login.add_argument('-f', '--sessionfile',
|
||||||
help='Path for loading and storing session key file. '
|
help='Path for loading and storing session key file. '
|
||||||
'Defaults to ' + get_default_session_filename("<login_name>"))
|
'Defaults to ' + get_default_session_filename("<login_name>"))
|
||||||
@@ -328,10 +478,25 @@ def main():
|
|||||||
'{target} is replaced by the target you specified, i.e. either :feed, #hashtag or the '
|
'{target} is replaced by the target you specified, i.e. either :feed, #hashtag or the '
|
||||||
'profile name. Defaults to \'{target}\'.')
|
'profile name. Defaults to \'{target}\'.')
|
||||||
g_how.add_argument('--filename-pattern',
|
g_how.add_argument('--filename-pattern',
|
||||||
help='Prefix of filenames, relative to the directory given with '
|
help='Prefix of filenames for posts and stories, relative to the directory given with '
|
||||||
'--dirname-pattern. {profile} is replaced by the profile name,'
|
'--dirname-pattern. {profile} is replaced by the profile name,'
|
||||||
'{target} is replaced by the target you specified, i.e. either :feed'
|
'{target} is replaced by the target you specified, i.e. either :feed'
|
||||||
'#hashtag or the profile name. Defaults to \'{date_utc}_UTC\'')
|
'#hashtag or the profile name. Defaults to \'{date_utc}_UTC\'')
|
||||||
|
g_how.add_argument('--title-pattern',
|
||||||
|
help='Prefix of filenames for profile pics, hashtag profile pics, and highlight covers. '
|
||||||
|
'Defaults to \'{date_utc}_UTC_{typename}\' if --dirname-pattern contains \'{target}\' '
|
||||||
|
'or \'{dirname}\', or if --dirname-pattern is not specified. Otherwise defaults to '
|
||||||
|
'\'{target}_{date_utc}_UTC_{typename}\'.')
|
||||||
|
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('--sanitize-paths', action='store_true',
|
||||||
|
help='Sanitize paths so that the resulting file and directory names are valid on both '
|
||||||
|
'Windows and Unix.')
|
||||||
|
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',
|
g_how.add_argument('--user-agent',
|
||||||
help='User Agent to use for HTTP requests. Defaults to \'{}\'.'.format(default_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('-S', '--no-sleep', action='store_true', help=SUPPRESS)
|
||||||
@@ -339,12 +504,14 @@ def main():
|
|||||||
help='Maximum number of connection attempts until a request is aborted. Defaults to 3. If a '
|
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 '
|
'connection fails, it can be manually skipped by hitting CTRL+C. Set this to 0 to retry '
|
||||||
'infinitely.')
|
'infinitely.')
|
||||||
g_how.add_argument('--commit-mode', action='store_true',
|
g_how.add_argument('--commit-mode', action='store_true', help=SUPPRESS)
|
||||||
help='Tries to ensure downloaded images avoid corruption in case of unexpected interruption. '
|
g_how.add_argument('--request-timeout', metavar='N', type=float, default=300.0,
|
||||||
'If the last picture is corrupted, Instaloader will fix the picture the next time it is run. '
|
help='Seconds to wait before timing out a connection request. Defaults to 300.')
|
||||||
'Requires the JSON metadata to be saved.')
|
g_how.add_argument('--abort-on', type=http_status_code_list, metavar="STATUS_CODES",
|
||||||
g_how.add_argument('--request-timeout', metavar='N', type=float,
|
help='Comma-separated list of HTTP status codes that cause Instaloader to abort, bypassing all '
|
||||||
help='seconds to wait before timing out a connection request')
|
'retry logic.')
|
||||||
|
g_how.add_argument('--no-iphone', action='store_true',
|
||||||
|
help='Do not attempt to download iPhone version of images and videos.')
|
||||||
|
|
||||||
g_misc = parser.add_argument_group('Miscellaneous Options')
|
g_misc = parser.add_argument_group('Miscellaneous Options')
|
||||||
g_misc.add_argument('-q', '--quiet', action='store_true',
|
g_misc.add_argument('-q', '--quiet', action='store_true',
|
||||||
@@ -357,15 +524,15 @@ def main():
|
|||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
try:
|
try:
|
||||||
if args.login is None and (args.stories or args.stories_only):
|
if (args.login is None and args.load_cookies is None) and (args.stories or args.stories_only):
|
||||||
print("--login=USERNAME required to download stories.", file=sys.stderr)
|
print("Login is required to download stories.", file=sys.stderr)
|
||||||
args.stories = False
|
args.stories = False
|
||||||
if args.stories_only:
|
if args.stories_only:
|
||||||
raise SystemExit(1)
|
raise InvalidArgumentException()
|
||||||
|
|
||||||
if ':feed-all' in args.profile or ':feed-liked' in args.profile:
|
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 "
|
raise InvalidArgumentException(":feed-all and :feed-liked were removed. Use :feed as target and "
|
||||||
"eventually --post-filter=viewer_has_liked.")
|
"eventually --post-filter=viewer_has_liked.")
|
||||||
|
|
||||||
post_metadata_txt_pattern = '\n'.join(args.post_metadata_txt) if args.post_metadata_txt else None
|
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
|
storyitem_metadata_txt_pattern = '\n'.join(args.storyitem_metadata_txt) if args.storyitem_metadata_txt else None
|
||||||
@@ -375,20 +542,24 @@ def main():
|
|||||||
post_metadata_txt_pattern = ''
|
post_metadata_txt_pattern = ''
|
||||||
storyitem_metadata_txt_pattern = ''
|
storyitem_metadata_txt_pattern = ''
|
||||||
else:
|
else:
|
||||||
raise SystemExit("--no-captions and --post-metadata-txt or --storyitem-metadata-txt given; "
|
raise InvalidArgumentException("--no-captions and --post-metadata-txt or --storyitem-metadata-txt "
|
||||||
"That contradicts.")
|
"given; That contradicts.")
|
||||||
|
|
||||||
|
if args.no_resume and args.resume_prefix:
|
||||||
|
raise InvalidArgumentException("--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:
|
if args.no_pictures and args.fast_update:
|
||||||
raise SystemExit('--no-pictures and --fast-update cannot be used together.')
|
raise InvalidArgumentException('--no-pictures and --fast-update cannot be used together.')
|
||||||
|
|
||||||
|
if args.login and args.load_cookies:
|
||||||
|
raise InvalidArgumentException('--load-cookies and --login cannot be used together.')
|
||||||
|
|
||||||
# Determine what to download
|
# Determine what to download
|
||||||
download_profile_pic = not args.no_profile_pic or args.profile_pic_only
|
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_posts = not (args.no_posts or args.stories_only or args.profile_pic_only)
|
||||||
download_stories = args.stories or args.stories_only
|
download_stories = args.stories or args.stories_only
|
||||||
|
|
||||||
if args.commit_mode and args.no_metadata_json:
|
|
||||||
raise SystemExit('--commit-mode requires JSON metadata to be saved.')
|
|
||||||
|
|
||||||
loader = Instaloader(sleep=not args.no_sleep, quiet=args.quiet, user_agent=args.user_agent,
|
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,
|
dirname_pattern=args.dirname_pattern, filename_pattern=args.filename_pattern,
|
||||||
download_pictures=not args.no_pictures,
|
download_pictures=not args.no_pictures,
|
||||||
@@ -400,24 +571,45 @@ def main():
|
|||||||
storyitem_metadata_txt_pattern=storyitem_metadata_txt_pattern,
|
storyitem_metadata_txt_pattern=storyitem_metadata_txt_pattern,
|
||||||
max_connection_attempts=args.max_connection_attempts,
|
max_connection_attempts=args.max_connection_attempts,
|
||||||
request_timeout=args.request_timeout,
|
request_timeout=args.request_timeout,
|
||||||
commit_mode=args.commit_mode)
|
resume_prefix=resume_prefix,
|
||||||
_main(loader,
|
check_resume_bbd=not args.use_aged_resume_files,
|
||||||
args.profile,
|
slide=args.slide,
|
||||||
username=args.login.lower() if args.login is not None else None,
|
fatal_status_codes=args.abort_on,
|
||||||
password=args.password,
|
iphone_support=not args.no_iphone,
|
||||||
sessionfile=args.sessionfile,
|
title_pattern=args.title_pattern,
|
||||||
download_profile_pic=download_profile_pic,
|
sanitize_paths=args.sanitize_paths)
|
||||||
download_posts=download_posts,
|
exit_code = _main(loader,
|
||||||
download_stories=download_stories,
|
args.profile,
|
||||||
download_highlights=args.highlights,
|
username=args.login.lower() if args.login is not None else None,
|
||||||
download_tagged=args.tagged,
|
password=args.password,
|
||||||
fast_update=args.fast_update,
|
sessionfile=args.sessionfile,
|
||||||
max_count=int(args.count) if args.count is not None else None,
|
download_profile_pic=download_profile_pic,
|
||||||
post_filter_str=args.post_filter,
|
download_posts=download_posts,
|
||||||
storyitem_filter_str=args.storyitem_filter)
|
download_stories=download_stories,
|
||||||
|
download_highlights=args.highlights,
|
||||||
|
download_tagged=args.tagged,
|
||||||
|
download_reels=args.reels,
|
||||||
|
download_igtv=args.igtv,
|
||||||
|
fast_update=args.fast_update,
|
||||||
|
latest_stamps_file=args.latest_stamps,
|
||||||
|
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,
|
||||||
|
browser=args.load_cookies,
|
||||||
|
cookiefile=args.cookiefile)
|
||||||
loader.close()
|
loader.close()
|
||||||
|
if loader.has_stored_errors:
|
||||||
|
exit_code = ExitCode.NON_FATAL_ERROR
|
||||||
|
except InvalidArgumentException as err:
|
||||||
|
print(err, file=sys.stderr)
|
||||||
|
exit_code = ExitCode.INIT_FAILURE
|
||||||
|
except LoginException as err:
|
||||||
|
print(err, file=sys.stderr)
|
||||||
|
exit_code = ExitCode.LOGIN_FAILURE
|
||||||
except InstaloaderException as err:
|
except InstaloaderException as err:
|
||||||
raise SystemExit("Fatal error: %s" % err)
|
print("Fatal error: %s" % err)
|
||||||
|
exit_code = ExitCode.UNEXPECTED_ERROR
|
||||||
|
sys.exit(exit_code)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ class LoginRequiredException(InstaloaderException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TwoFactorAuthRequiredException(InstaloaderException):
|
class LoginException(InstaloaderException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TwoFactorAuthRequiredException(LoginException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -45,7 +49,7 @@ class BadResponseException(InstaloaderException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BadCredentialsException(InstaloaderException):
|
class BadCredentialsException(LoginException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -64,3 +68,17 @@ class QueryReturnedNotFoundException(ConnectionException):
|
|||||||
|
|
||||||
class TooManyRequestsException(ConnectionException):
|
class TooManyRequestsException(ConnectionException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class IPhoneSupportDisabledException(InstaloaderException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AbortDownloadException(Exception):
|
||||||
|
"""
|
||||||
|
Exception that is not catched in the error catchers inside the download loop and so aborts the
|
||||||
|
download loop.
|
||||||
|
|
||||||
|
This exception is not a subclass of ``InstaloaderException``.
|
||||||
|
|
||||||
|
.. versionadded:: 4.7
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|||||||
+787
-337
File diff suppressed because it is too large
Load Diff
+560
-203
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
|||||||
|
import configparser
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Optional
|
||||||
|
from os.path import dirname
|
||||||
|
from os import makedirs
|
||||||
|
|
||||||
|
|
||||||
|
class LatestStamps:
|
||||||
|
"""LatestStamps class.
|
||||||
|
|
||||||
|
Convenience class for retrieving and storing data from the :option:`--latest-stamps` file.
|
||||||
|
|
||||||
|
:param latest_stamps_file: path to file.
|
||||||
|
|
||||||
|
.. versionadded:: 4.8"""
|
||||||
|
PROFILE_ID = 'profile-id'
|
||||||
|
PROFILE_PIC = 'profile-pic'
|
||||||
|
POST_TIMESTAMP = 'post-timestamp'
|
||||||
|
TAGGED_TIMESTAMP = 'tagged-timestamp'
|
||||||
|
IGTV_TIMESTAMP = 'igtv-timestamp'
|
||||||
|
REELS_TIMESTAMP = 'reels-timestamp'
|
||||||
|
STORY_TIMESTAMP = 'story-timestamp'
|
||||||
|
ISO_FORMAT = '%Y-%m-%dT%H:%M:%S.%f%z'
|
||||||
|
|
||||||
|
def __init__(self, latest_stamps_file):
|
||||||
|
self.file = latest_stamps_file
|
||||||
|
self.data = configparser.ConfigParser()
|
||||||
|
self.data.read(latest_stamps_file)
|
||||||
|
|
||||||
|
def _save(self):
|
||||||
|
if dn := dirname(self.file):
|
||||||
|
makedirs(dn, exist_ok=True)
|
||||||
|
with open(self.file, 'w') as f:
|
||||||
|
self.data.write(f)
|
||||||
|
|
||||||
|
def _ensure_section(self, section: str):
|
||||||
|
if not self.data.has_section(section):
|
||||||
|
self.data.add_section(section)
|
||||||
|
|
||||||
|
def get_profile_id(self, profile_name: str) -> Optional[int]:
|
||||||
|
"""Returns stored ID of profile."""
|
||||||
|
try:
|
||||||
|
return self.data.getint(profile_name, self.PROFILE_ID)
|
||||||
|
except (configparser.Error, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def save_profile_id(self, profile_name: str, profile_id: int):
|
||||||
|
"""Stores ID of profile."""
|
||||||
|
self._ensure_section(profile_name)
|
||||||
|
self.data.set(profile_name, self.PROFILE_ID, str(profile_id))
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
def rename_profile(self, old_profile: str, new_profile: str):
|
||||||
|
"""Renames a profile."""
|
||||||
|
self._ensure_section(new_profile)
|
||||||
|
for option in [self.PROFILE_ID, self.PROFILE_PIC, self.POST_TIMESTAMP,
|
||||||
|
self.TAGGED_TIMESTAMP, self.IGTV_TIMESTAMP, self.STORY_TIMESTAMP]:
|
||||||
|
if self.data.has_option(old_profile, option):
|
||||||
|
value = self.data.get(old_profile, option)
|
||||||
|
self.data.set(new_profile, option, value)
|
||||||
|
self.data.remove_section(old_profile)
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
def _get_timestamp(self, section: str, key: str) -> datetime:
|
||||||
|
try:
|
||||||
|
return datetime.strptime(self.data.get(section, key), self.ISO_FORMAT)
|
||||||
|
except (configparser.Error, ValueError):
|
||||||
|
return datetime.fromtimestamp(0, timezone.utc)
|
||||||
|
|
||||||
|
def _set_timestamp(self, section: str, key: str, timestamp: datetime):
|
||||||
|
self._ensure_section(section)
|
||||||
|
self.data.set(section, key, timestamp.strftime(self.ISO_FORMAT))
|
||||||
|
self._save()
|
||||||
|
|
||||||
|
def get_last_post_timestamp(self, profile_name: str) -> datetime:
|
||||||
|
"""Returns timestamp of last download of a profile's posts."""
|
||||||
|
return self._get_timestamp(profile_name, self.POST_TIMESTAMP)
|
||||||
|
|
||||||
|
def set_last_post_timestamp(self, profile_name: str, timestamp: datetime):
|
||||||
|
"""Sets timestamp of last download of a profile's posts."""
|
||||||
|
self._set_timestamp(profile_name, self.POST_TIMESTAMP, timestamp)
|
||||||
|
|
||||||
|
def get_last_tagged_timestamp(self, profile_name: str) -> datetime:
|
||||||
|
"""Returns timestamp of last download of a profile's tagged posts."""
|
||||||
|
return self._get_timestamp(profile_name, self.TAGGED_TIMESTAMP)
|
||||||
|
|
||||||
|
def set_last_tagged_timestamp(self, profile_name: str, timestamp: datetime):
|
||||||
|
"""Sets timestamp of last download of a profile's tagged posts."""
|
||||||
|
self._set_timestamp(profile_name, self.TAGGED_TIMESTAMP, timestamp)
|
||||||
|
|
||||||
|
def get_last_reels_timestamp(self, profile_name: str) -> datetime:
|
||||||
|
"""Returns timestamp of last download of a profile's reels posts."""
|
||||||
|
return self._get_timestamp(profile_name, self.REELS_TIMESTAMP)
|
||||||
|
|
||||||
|
def set_last_reels_timestamp(self, profile_name: str, timestamp: datetime):
|
||||||
|
"""Sets timestamp of last download of a profile's reels posts."""
|
||||||
|
self._set_timestamp(profile_name, self.REELS_TIMESTAMP, timestamp)
|
||||||
|
|
||||||
|
def get_last_igtv_timestamp(self, profile_name: str) -> datetime:
|
||||||
|
"""Returns timestamp of last download of a profile's igtv posts."""
|
||||||
|
return self._get_timestamp(profile_name, self.IGTV_TIMESTAMP)
|
||||||
|
|
||||||
|
def set_last_igtv_timestamp(self, profile_name: str, timestamp: datetime):
|
||||||
|
"""Sets timestamp of last download of a profile's igtv posts."""
|
||||||
|
self._set_timestamp(profile_name, self.IGTV_TIMESTAMP, timestamp)
|
||||||
|
|
||||||
|
def get_last_story_timestamp(self, profile_name: str) -> datetime:
|
||||||
|
"""Returns timestamp of last download of a profile's stories."""
|
||||||
|
return self._get_timestamp(profile_name, self.STORY_TIMESTAMP)
|
||||||
|
|
||||||
|
def set_last_story_timestamp(self, profile_name: str, timestamp: datetime):
|
||||||
|
"""Sets timestamp of last download of a profile's stories."""
|
||||||
|
self._set_timestamp(profile_name, self.STORY_TIMESTAMP, timestamp)
|
||||||
|
|
||||||
|
def get_profile_pic(self, profile_name: str) -> str:
|
||||||
|
"""Returns filename of profile's last downloaded profile pic."""
|
||||||
|
try:
|
||||||
|
return self.data.get(profile_name, self.PROFILE_PIC)
|
||||||
|
except configparser.Error:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def set_profile_pic(self, profile_name: str, profile_pic: str):
|
||||||
|
"""Sets filename of profile's last downloaded profile pic."""
|
||||||
|
self._ensure_section(profile_name)
|
||||||
|
self.data.set(profile_name, self.PROFILE_PIC, profile_pic)
|
||||||
|
self._save()
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
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, Iterable, Iterator, NamedTuple, Optional, Tuple, TypeVar
|
||||||
|
|
||||||
|
from .exceptions import InvalidArgumentException
|
||||||
|
from .instaloadercontext import InstaloaderContext
|
||||||
|
|
||||||
|
class FrozenNodeIterator(NamedTuple):
|
||||||
|
query_hash: Optional[str]
|
||||||
|
query_variables: Dict
|
||||||
|
query_referer: Optional[str]
|
||||||
|
context_username: Optional[str]
|
||||||
|
total_index: int
|
||||||
|
best_before: Optional[float]
|
||||||
|
remaining_data: Optional[Dict]
|
||||||
|
first_node: Optional[Dict]
|
||||||
|
doc_id: Optional[str]
|
||||||
|
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."""
|
||||||
|
FrozenNodeIterator.first_node.__doc__ = """Node data of the first item, if an item has been produced."""
|
||||||
|
FrozenNodeIterator.doc_id.__doc__ = """The GraphQL ``doc_id`` parameter."""
|
||||||
|
|
||||||
|
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"))
|
||||||
|
|
||||||
|
(an appropriate method to load and save the :class:`FrozenNodeIterator` is e.g.
|
||||||
|
:func:`load_structure_from_file` and :func:`save_structure_to_file`.)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
.. versionchanged: 4.13
|
||||||
|
Included support for `doc_id`-based queries (using POST method).
|
||||||
|
"""
|
||||||
|
|
||||||
|
_graphql_page_length = 12
|
||||||
|
_shelf_life = timedelta(days=29)
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
context: InstaloaderContext,
|
||||||
|
query_hash: Optional[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,
|
||||||
|
is_first: Optional[Callable[[T, Optional[T]], bool]] = None,
|
||||||
|
doc_id: Optional[str] = None):
|
||||||
|
self._context = context
|
||||||
|
self._query_hash = query_hash
|
||||||
|
self._doc_id = doc_id
|
||||||
|
self._edge_extractor = edge_extractor
|
||||||
|
self._node_wrapper = node_wrapper
|
||||||
|
self._query_variables = query_variables if query_variables is not None else {}
|
||||||
|
self._query_referer = query_referer
|
||||||
|
self._page_index = 0
|
||||||
|
self._total_index = 0
|
||||||
|
if first_data is not None:
|
||||||
|
self._data = first_data
|
||||||
|
self._best_before = datetime.now() + NodeIterator._shelf_life
|
||||||
|
else:
|
||||||
|
self._data = self._query()
|
||||||
|
self._first_node: Optional[Dict] = None
|
||||||
|
self._is_first = is_first
|
||||||
|
|
||||||
|
def _query(self, after: Optional[str] = None) -> Dict:
|
||||||
|
if self._doc_id is not None:
|
||||||
|
return self._query_doc_id(self._doc_id, after)
|
||||||
|
else:
|
||||||
|
assert self._query_hash is not None
|
||||||
|
return self._query_query_hash(self._query_hash, after)
|
||||||
|
|
||||||
|
def _query_doc_id(self, doc_id: str, after: Optional[str] = None) -> Dict:
|
||||||
|
pagination_variables: Dict[str, Any] = {'__relay_internal__pv__PolarisFeedShareMenurelayprovider': False}
|
||||||
|
if after is not None:
|
||||||
|
pagination_variables['after'] = after
|
||||||
|
pagination_variables['before'] = None
|
||||||
|
pagination_variables['first'] = 12
|
||||||
|
pagination_variables['last'] = None
|
||||||
|
data = self._edge_extractor(
|
||||||
|
self._context.doc_id_graphql_query(
|
||||||
|
doc_id, {**self._query_variables, **pagination_variables}, self._query_referer
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._best_before = datetime.now() + NodeIterator._shelf_life
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _query_query_hash(self, query_hash: str, after: Optional[str] = None) -> Dict:
|
||||||
|
pagination_variables: Dict[str, Any] = {'first': NodeIterator._graphql_page_length}
|
||||||
|
if after is not None:
|
||||||
|
pagination_variables['after'] = after
|
||||||
|
data = self._edge_extractor(
|
||||||
|
self._context.graphql_query(
|
||||||
|
query_hash, {**self._query_variables, **pagination_variables}, self._query_referer
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self._best_before = datetime.now() + NodeIterator._shelf_life
|
||||||
|
return data
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self) -> T:
|
||||||
|
if self._page_index < len(self._data['edges']):
|
||||||
|
node = self._data['edges'][self._page_index]['node']
|
||||||
|
page_index, total_index = self._page_index, self._total_index
|
||||||
|
try:
|
||||||
|
self._page_index += 1
|
||||||
|
self._total_index += 1
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self._page_index, self._total_index = page_index, total_index
|
||||||
|
raise
|
||||||
|
item = self._node_wrapper(node)
|
||||||
|
if self._is_first is not None:
|
||||||
|
if self._is_first(item, self.first_item):
|
||||||
|
self._first_node = node
|
||||||
|
else:
|
||||||
|
if self._first_node is None:
|
||||||
|
self._first_node = node
|
||||||
|
return item
|
||||||
|
if self._data.get('page_info', {}).get('has_next_page'):
|
||||||
|
query_response = self._query(self._data['page_info']['end_cursor'])
|
||||||
|
if self._data['edges'] != query_response['edges'] and len(query_response['edges']) > 0:
|
||||||
|
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)."""
|
||||||
|
magic_hash = hashlib.blake2b(digest_size=6)
|
||||||
|
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()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def first_item(self) -> Optional[T]:
|
||||||
|
"""
|
||||||
|
If this iterator has produced any items, returns the first item produced.
|
||||||
|
|
||||||
|
It is possible to override what is considered the first item (for example, to consider the
|
||||||
|
newest item in case items are not in strict chronological order) by passing a callback
|
||||||
|
function as the `is_first` parameter when creating the class.
|
||||||
|
|
||||||
|
.. versionadded:: 4.8
|
||||||
|
.. versionchanged:: 4.9.2
|
||||||
|
What is considered the first item can be overridden.
|
||||||
|
"""
|
||||||
|
return self._node_wrapper(self._first_node) if self._first_node is not None else None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def page_length() -> int:
|
||||||
|
return NodeIterator._graphql_page_length
|
||||||
|
|
||||||
|
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,
|
||||||
|
first_node=self._first_node,
|
||||||
|
doc_id=self._doc_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
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 or
|
||||||
|
self._doc_id != frozen.doc_id):
|
||||||
|
raise InvalidArgumentException("Mismatching resume information.")
|
||||||
|
if not frozen.best_before:
|
||||||
|
raise InvalidArgumentException("\"best before\" date missing.")
|
||||||
|
if frozen.remaining_data is None:
|
||||||
|
raise InvalidArgumentException("\"remaining_data\" missing.")
|
||||||
|
self._total_index = frozen.total_index
|
||||||
|
self._best_before = datetime.fromtimestamp(frozen.best_before)
|
||||||
|
self._data = frozen.remaining_data
|
||||||
|
if frozen.first_node is not None:
|
||||||
|
self._first_node = frozen.first_node
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def resumable_iteration(context: InstaloaderContext,
|
||||||
|
iterator: Iterable,
|
||||||
|
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 :class:`KeyboardInterrupt` or an :class:`AbortDownloadException`.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.7
|
||||||
|
Also interrupt on :class:`AbortDownloadException`.
|
||||||
|
"""
|
||||||
|
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, EOFError) as exc:
|
||||||
|
context.error("Warning: Not resuming from {}: {}".format(resume_file_path, exc))
|
||||||
|
try:
|
||||||
|
yield is_resuming, start_index
|
||||||
|
except (Exception, KeyboardInterrupt):
|
||||||
|
if os.path.dirname(resume_file_path):
|
||||||
|
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))
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
from typing import Any, Callable, Dict, Iterator, Optional, TypeVar
|
||||||
|
|
||||||
|
from .instaloadercontext import InstaloaderContext
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
|
class SectionIterator(Iterator[T]):
|
||||||
|
"""Iterator for the new 'sections'-style responses.
|
||||||
|
|
||||||
|
.. versionadded:: 4.9"""
|
||||||
|
def __init__(self,
|
||||||
|
context: InstaloaderContext,
|
||||||
|
sections_extractor: Callable[[Dict[str, Any]], Dict[str, Any]],
|
||||||
|
media_wrapper: Callable[[Dict], T],
|
||||||
|
query_path: str,
|
||||||
|
first_data: Optional[Dict[str, Any]] = None):
|
||||||
|
self._context = context
|
||||||
|
self._sections_extractor = sections_extractor
|
||||||
|
self._media_wrapper = media_wrapper
|
||||||
|
self._query_path = query_path
|
||||||
|
self._data = first_data or self._query()
|
||||||
|
self._page_index = 0
|
||||||
|
self._section_index = 0
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _query(self, max_id: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
pagination_variables = {"max_id": max_id} if max_id is not None else {}
|
||||||
|
return self._sections_extractor(
|
||||||
|
self._context.get_json(self._query_path, params={"__a": 1, "__d": "dis", **pagination_variables})
|
||||||
|
)
|
||||||
|
|
||||||
|
def __next__(self) -> T:
|
||||||
|
if self._page_index < len(self._data['sections']):
|
||||||
|
media = self._data['sections'][self._page_index]['layout_content']['medias'][self._section_index]['media']
|
||||||
|
self._section_index += 1
|
||||||
|
if self._section_index >= len(self._data['sections'][self._page_index]['layout_content']['medias']):
|
||||||
|
self._section_index = 0
|
||||||
|
self._page_index += 1
|
||||||
|
return self._media_wrapper(media)
|
||||||
|
if self._data['more_available']:
|
||||||
|
self._page_index, self._section_index, self._data = 0, 0, self._query(self._data["next_max_id"])
|
||||||
|
return self.__next__()
|
||||||
|
raise StopIteration()
|
||||||
+1468
-204
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import platform
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
|
|
||||||
@@ -19,13 +18,13 @@ def get_version():
|
|||||||
raise SystemExit("Could not find version string.")
|
raise SystemExit("Could not find version string.")
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info < (3, 5):
|
if sys.version_info < (3, 10):
|
||||||
sys.exit('Instaloader requires Python >= 3.5.')
|
sys.exit('Instaloader requires Python >= 3.10.')
|
||||||
|
|
||||||
requirements = ['requests>=2.4']
|
requirements = ['requests>=2.25']
|
||||||
|
optional_requirements = {
|
||||||
if platform.system() == 'Windows' and sys.version_info < (3, 6):
|
'browser_cookie3': ['browser_cookie3>=0.19.1'],
|
||||||
requirements.append('win_unicode_console')
|
}
|
||||||
|
|
||||||
keywords = (['instagram', 'instagram-scraper', 'instagram-client', 'instagram-feed', 'downloader', 'videos', 'photos',
|
keywords = (['instagram', 'instagram-scraper', 'instagram-client', 'instagram-feed', 'downloader', 'videos', 'photos',
|
||||||
'pictures', 'instagram-user-photos', 'instagram-photos', 'instagram-metadata', 'instagram-downloader',
|
'pictures', 'instagram-user-photos', 'instagram-photos', 'instagram-metadata', 'instagram-downloader',
|
||||||
@@ -38,6 +37,7 @@ setup(
|
|||||||
name='instaloader',
|
name='instaloader',
|
||||||
version=get_version(),
|
version=get_version(),
|
||||||
packages=['instaloader'],
|
packages=['instaloader'],
|
||||||
|
package_data={'instaloader': ['py.typed']},
|
||||||
url='https://instaloader.github.io/',
|
url='https://instaloader.github.io/',
|
||||||
license='MIT',
|
license='MIT',
|
||||||
author='Alexander Graf, André Koch-Kramer',
|
author='Alexander Graf, André Koch-Kramer',
|
||||||
@@ -46,9 +46,10 @@ setup(
|
|||||||
'from Instagram.',
|
'from Instagram.',
|
||||||
long_description=open(os.path.join(SRC, 'README.rst')).read(),
|
long_description=open(os.path.join(SRC, 'README.rst')).read(),
|
||||||
install_requires=requirements,
|
install_requires=requirements,
|
||||||
python_requires='>=3.5',
|
python_requires='>=3.10',
|
||||||
|
extras_require=optional_requirements,
|
||||||
entry_points={'console_scripts': ['instaloader=instaloader.__main__:main']},
|
entry_points={'console_scripts': ['instaloader=instaloader.__main__:main']},
|
||||||
zip_safe=True,
|
zip_safe=False,
|
||||||
keywords=keywords,
|
keywords=keywords,
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 5 - Production/Stable',
|
'Development Status :: 5 - Production/Stable',
|
||||||
@@ -57,10 +58,12 @@ setup(
|
|||||||
'Intended Audience :: Developers',
|
'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.10',
|
||||||
'Programming Language :: Python :: 3.6',
|
'Programming Language :: Python :: 3.11',
|
||||||
'Programming Language :: Python :: 3.7',
|
'Programming Language :: Python :: 3.12',
|
||||||
'Programming Language :: Python :: 3.8',
|
'Programming Language :: Python :: 3.13',
|
||||||
|
'Programming Language :: Python :: 3.14',
|
||||||
|
'Programming Language :: Python :: 3.15',
|
||||||
'Programming Language :: Python :: 3 :: Only',
|
'Programming Language :: Python :: 3 :: Only',
|
||||||
'Topic :: Internet',
|
'Topic :: Internet',
|
||||||
'Topic :: Multimedia :: Graphics'
|
'Topic :: Multimedia :: Graphics'
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Instaloader Tests
|
||||||
|
|
||||||
|
Integration tests for Instaloader features that require Instagram access are maintained here:
|
||||||
|
https://github.com/instaloader/instaloader-tests
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
"""Unit Tests for Instaloader"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
import tempfile
|
|
||||||
import unittest
|
|
||||||
from itertools import islice
|
|
||||||
from typing import Dict, List
|
|
||||||
|
|
||||||
import instaloader
|
|
||||||
|
|
||||||
PROFILE_WITH_HIGHLIGHTS = 325732271
|
|
||||||
PUBLIC_PROFILE = "selenagomez"
|
|
||||||
PUBLIC_PROFILE_ID = 460563723
|
|
||||||
HASHTAG = "kitten"
|
|
||||||
LOCATION = "362629379"
|
|
||||||
OWN_USERNAME = "aandergr"
|
|
||||||
NORMAL_MAX_COUNT = 2
|
|
||||||
PAGING_MAX_COUNT = 15
|
|
||||||
PRIVATE_PROFILE = "aandergr"
|
|
||||||
PRIVATE_PROFILE_ID = 1706625676
|
|
||||||
EMPTY_PROFILE = "not_public"
|
|
||||||
EMPTY_PROFILE_ID = 1928659031
|
|
||||||
|
|
||||||
# Preserve query timestamps (rate control) between tests to not get rate limited
|
|
||||||
instaloadercontext_query_timestamps = dict() # type: Dict[str, List[float]]
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
# pylint:disable=protected-access
|
|
||||||
self.L.context._graphql_query_timestamps = instaloadercontext_query_timestamps.copy()
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
# pylint:disable=global-statement,protected-access
|
|
||||||
global instaloadercontext_query_timestamps
|
|
||||||
instaloadercontext_query_timestamps = self.L.context._graphql_query_timestamps.copy()
|
|
||||||
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(self.L.get_hashtag_posts(HASHTAG)):
|
|
||||||
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_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)
|
|
||||||
|
|
||||||
|
|
||||||
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_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