mirror of
https://github.com/instaloader/instaloader.git
synced 2026-10-29 04:31:55 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e232c82d5e | ||
|
|
601dd0b106 | ||
|
|
4c20b573e2 | ||
|
|
db63e5c7e5 | ||
|
|
e21d34124d | ||
|
|
66b3ec6928 | ||
|
|
f7d82495bd | ||
|
|
b0a430051e | ||
|
|
8b56b90e72 | ||
|
|
0c93e63615 | ||
|
|
65025e61d9 |
@@ -24,4 +24,4 @@ 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)
|
<!-- please also see https://instaloader.github.io/contributing.html for how to report a bug -->
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Contributing to Instaloader
|
||||||
|
url: https://instaloader.github.io/contributing.html
|
||||||
|
about: Guidelines for how to report an issue.
|
||||||
|
- name: Instaloader Troubleshooting
|
||||||
|
url: https://instaloader.github.io/troubleshooting.html
|
||||||
|
about: Many common problems and their workarounds are described here.
|
||||||
|
- name: Instaloader Documentation
|
||||||
|
url: https://instaloader.github.io/
|
||||||
|
about: Many questions might be already answered here.
|
||||||
@@ -15,10 +15,10 @@ A clear and concise description of what you want to happen.
|
|||||||
A clear and concise description of any alternative solutions or features you've considered.
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
**If the feature request is accepted, would you be willing to submit a PR?**
|
**If the feature request is accepted, would you be willing to submit a PR?**
|
||||||
|
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)
|
<!-- please also see https://instaloader.github.io/contributing.html for how to suggest a feature -->
|
||||||
|
|||||||
@@ -6,3 +6,7 @@ labels: question
|
|||||||
---
|
---
|
||||||
|
|
||||||
Your question here...
|
Your question here...
|
||||||
|
|
||||||
|
<!-- Please see our documentation: https://instaloader.github.io/ -->
|
||||||
|
|
||||||
|
<!-- Basic questions regarding Instaloader's usage might be more appropriate to ask on Stack Overflow -->
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please describe:
|
||||||
|
|
||||||
|
- A motivation for this change, e.g.
|
||||||
|
- Fixes # .
|
||||||
|
- More general: What problem does the pull request solve?
|
||||||
|
|
||||||
|
- The changes proposed in this pull request
|
||||||
|
|
||||||
|
- The completeness of this change
|
||||||
|
- Is it just a proof of concept?
|
||||||
|
- Is the documentation updated (if appropriate)?
|
||||||
|
- Do you consider it ready to be merged or is it a draft?
|
||||||
|
- Can we help you at some point?
|
||||||
|
-->
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: PyLint and MyPy
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: PyLint and MyPy
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
python-version: [3.8]
|
||||||
|
steps:
|
||||||
|
- name: Checkout Instaloader Repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # needed for building docs
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: ${{ matrix.python-version }}
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install pipenv==2020.6.2
|
||||||
|
pipenv sync --dev
|
||||||
|
- name: PyLint
|
||||||
|
run: pipenv run pylint instaloader
|
||||||
|
- name: MyPy
|
||||||
|
run: pipenv run mypy -m instaloader
|
||||||
|
- name: Build Documentation
|
||||||
|
run: pipenv run make -C docs html SPHINXOPTS="-W -n"
|
||||||
@@ -7,6 +7,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
stale:
|
stale:
|
||||||
|
|
||||||
|
name: Mark stale issues and pull requests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ deploy:
|
|||||||
repo: instaloader/instaloader.github.io
|
repo: instaloader/instaloader.github.io
|
||||||
target-branch: master
|
target-branch: master
|
||||||
on:
|
on:
|
||||||
tags: true
|
branch: master
|
||||||
python: 3.6
|
python: 3.6
|
||||||
- provider: script
|
- provider: script
|
||||||
script: deploy/arch/deploy.sh $TRAVIS_TAG
|
script: deploy/arch/deploy.sh $TRAVIS_TAG
|
||||||
|
|||||||
Vendored
+3
-1
@@ -17,7 +17,9 @@ code {
|
|||||||
.highlight pre {
|
.highlight pre {
|
||||||
padding: 0.7em;
|
padding: 0.7em;
|
||||||
color: #fff; }
|
color: #fff; }
|
||||||
.highlight .c1, .highlight .k, .highlight .kn, .highlight .ow {
|
.highlight .c1 {
|
||||||
|
color: #666; }
|
||||||
|
.highlight .k, .highlight .kn, .highlight .ow {
|
||||||
color: #008d06; }
|
color: #008d06; }
|
||||||
.highlight .nb, .highlight .ne, .highlight .nf, .highlight .vm {
|
.highlight .nb, .highlight .ne, .highlight .nf, .highlight .vm {
|
||||||
color: #f48400; }
|
color: #f48400; }
|
||||||
|
|||||||
Vendored
+5
-1
@@ -36,7 +36,11 @@ code {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c1, .k, .kn, .ow {
|
.c1 {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.k, .kn, .ow {
|
||||||
color: $color_instaloader_main
|
color: $color_instaloader_main
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,8 +111,12 @@ Instaloader supports the following targets:
|
|||||||
``profile`` (requires :option:`--login`).
|
``profile`` (requires :option:`--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 preceeded by ``--`` in
|
||||||
the argument list to not be mistaken as an option flag.
|
the argument list to not be mistaken as an option flag::
|
||||||
|
|
||||||
|
instaloader -- -B_K4CykAOtf
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.. versionadded:: 4.1
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -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,6 +38,14 @@ 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 an Hashtag feed.
|
||||||
|
|
||||||
|
.. literalinclude:: codesnippets/666_historical_hashtag_data.py
|
||||||
|
|
||||||
Likes of a Profile / Ghost Followers
|
Likes of a Profile / Ghost Followers
|
||||||
------------------------------------
|
------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,11 @@ import instaloader
|
|||||||
|
|
||||||
L = instaloader.Instaloader()
|
L = instaloader.Instaloader()
|
||||||
|
|
||||||
posts = instaloader.Hashtag.from_name(L.context, 'urbanphotography').get_posts()
|
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,30 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import instaloader
|
||||||
|
|
||||||
|
L = instaloader.Instaloader()
|
||||||
|
|
||||||
|
posts = instaloader.Hashtag.from_name(L.context, "urbanphotography").get_posts()
|
||||||
|
|
||||||
|
SINCE = datetime(2020, 5, 10) # further from today, inclusive
|
||||||
|
UNTIL = datetime(2020, 5, 11) # closer to today, not inclusive
|
||||||
|
|
||||||
|
k = 0 # initiate k
|
||||||
|
k_list = [] # uncomment this to tune k
|
||||||
|
|
||||||
|
for post in posts:
|
||||||
|
postdate = post.date
|
||||||
|
|
||||||
|
if postdate > UNTIL:
|
||||||
|
continue
|
||||||
|
elif postdate <= SINCE:
|
||||||
|
k += 1
|
||||||
|
if k == 50:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
L.download_post(post, "#urbanphotography")
|
||||||
|
k = 0 # set k to 0
|
||||||
|
# if you want to tune k, uncomment below to get your k max
|
||||||
|
#k_list.append(k)
|
||||||
|
#max(k_list)
|
||||||
+27
-26
@@ -35,8 +35,8 @@ reporting a problem, please keep the following in mind:
|
|||||||
- If not obvious, describe **which behavior you expected**
|
- If not 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
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
@@ -45,28 +45,25 @@ Changes of the Instaloader source can be proposed as a
|
|||||||
`Pull Request <https://github.com/instaloader/instaloader/pulls>`__. There are only
|
`Pull Request <https://github.com/instaloader/instaloader/pulls>`__. There are only
|
||||||
few things to consider:
|
few things to consider:
|
||||||
|
|
||||||
- Sometimes, the most current code is not in the ``master`` branch. Check that
|
- Base your Pull Request on the ``master`` branch if it fixes a bug,
|
||||||
you forked from the most recent branch.
|
or the ``upcoming/v4.X`` branch (if it exists at the moment of submitting the PR)
|
||||||
|
otherwise.
|
||||||
|
|
||||||
- We use `Pylint <https://www.pylint.org/>`__ for error and syntax checking of
|
- We use `Pylint <https://www.pylint.org/>`__ for error and syntax checking of
|
||||||
the source. The file ``.travis.yml`` in the project's root directory
|
the source and `MyPy <https://github.com/python/mypy>`__ for type checking.
|
||||||
shows how it is invoked. Note that sometimes it might be better to disable a
|
Beware that sometimes it might be better to disable a warning rather than
|
||||||
warning rather than adapting the code to Pylint's desires.
|
adapting the code to a tool's desires.
|
||||||
|
|
||||||
- The documentation source is located in the ``docs`` folder. The file
|
- The documentation source is located in the ``docs`` folder. The file
|
||||||
``cli-options.rst`` is merely an RST-formatted copy of ``instaloader --help``
|
``cli-options.rst`` is merely an RST-formatted copy of ``instaloader --help``
|
||||||
output, of which the source is in ``instaloader/__main__.py``.
|
output, of which the source is in ``instaloader/__main__.py``.
|
||||||
|
|
||||||
- Feel free to contact us, even if you "only" have Proof-of-Concepts or
|
Proposing Features
|
||||||
not-fully integrated changes. They already might be an advance for the
|
------------------
|
||||||
project.
|
|
||||||
|
|
||||||
Suggesting 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 +71,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,16 +82,21 @@ 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.
|
|
||||||
|
- Briefly describe how your suggested feature **conforms with Instaloader's
|
||||||
|
project goal**.
|
||||||
|
|
||||||
|
- Explain your **solution ideas**. Describe your ideas on how the feature could
|
||||||
|
be implemented and the underlying problem could be solved. Also **describe
|
||||||
|
alternatives** that you have considered.
|
||||||
|
|
||||||
Donations
|
Donations
|
||||||
---------
|
---------
|
||||||
|
|||||||
@@ -27,11 +27,25 @@ expired. In case the retry continuously fails for some reason, which should not
|
|||||||
happen in normal conditions, consider adjusting the
|
happen in 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
|
||||||
|
promiscious IP addresses, such as cloud, VPN and public proxy services, might be
|
||||||
|
subject to significantly stricter limits for anonymous access. However,
|
||||||
|
logged-in accesses (see :option:`--login`) do not seem to be affected.
|
||||||
|
|
||||||
|
Too many queries in the last time
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
**"Too many queries in the last time"** is not an error. It is a notice that the
|
**"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. We regularly adjust this mechanism to match Instagram's
|
||||||
current rate limiting.
|
current rate limiting.
|
||||||
|
|
||||||
|
Private but not followed
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
You have to follow a private account to access most of its associated
|
||||||
|
information.
|
||||||
|
|
||||||
Login Error
|
Login Error
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
|||||||
@@ -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.4.4'
|
__version__ = '4.4.5'
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
+15
-13
@@ -393,12 +393,12 @@ class Instaloader:
|
|||||||
if ((format_string_contains_key(self.dirname_pattern, 'profile') or
|
if ((format_string_contains_key(self.dirname_pattern, 'profile') or
|
||||||
format_string_contains_key(self.dirname_pattern, 'target'))):
|
format_string_contains_key(self.dirname_pattern, 'target'))):
|
||||||
profile_str = owner_profile.username.lower() if owner_profile is not None else target
|
profile_str = owner_profile.username.lower() if owner_profile is not None else target
|
||||||
filename = '{0}/{1}_{2}.{3}'.format(self.dirname_pattern.format(profile=profile_str,
|
filename = os.path.join(self.dirname_pattern.format(profile=profile_str,
|
||||||
target=target),
|
target=target),
|
||||||
pic_identifier, name_suffix, pic_extension)
|
'{0}_{1}.{2}'.format(pic_identifier, name_suffix, pic_extension))
|
||||||
else:
|
else:
|
||||||
filename = '{0}/{1}_{2}_{3}.{4}'.format(self.dirname_pattern.format(), target,
|
filename = os.path.join(self.dirname_pattern.format(),
|
||||||
pic_identifier, name_suffix, pic_extension)
|
'{0}_{1}_{2}.{3}'.format(target, pic_identifier, name_suffix, pic_extension))
|
||||||
content_length = http_response.headers.get('Content-Length', None)
|
content_length = http_response.headers.get('Content-Length', None)
|
||||||
if os.path.isfile(filename) and (not self.context.is_logged_in or
|
if os.path.isfile(filename) and (not self.context.is_logged_in or
|
||||||
(content_length is not None and
|
(content_length is not None and
|
||||||
@@ -501,7 +501,7 @@ class Instaloader:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
dirname = _PostPathFormatter(post).format(self.dirname_pattern, target=target)
|
dirname = _PostPathFormatter(post).format(self.dirname_pattern, target=target)
|
||||||
filename = dirname + '/' + self.format_filename(post, target=target)
|
filename = os.path.join(dirname, self.format_filename(post, target=target))
|
||||||
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
||||||
|
|
||||||
# Download the image(s) / video thumbnail and videos within sidecars if desired
|
# Download the image(s) / video thumbnail and videos within sidecars if desired
|
||||||
@@ -630,7 +630,7 @@ class Instaloader:
|
|||||||
|
|
||||||
date_local = item.date_local
|
date_local = item.date_local
|
||||||
dirname = _PostPathFormatter(item).format(self.dirname_pattern, target=target)
|
dirname = _PostPathFormatter(item).format(self.dirname_pattern, target=target)
|
||||||
filename = dirname + '/' + self.format_filename(item, target=target)
|
filename = os.path.join(dirname, self.format_filename(item, target=target))
|
||||||
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
||||||
downloaded = False
|
downloaded = False
|
||||||
if not item.is_video or self.download_video_thumbnails is True:
|
if not item.is_video or self.download_video_thumbnails is True:
|
||||||
@@ -974,10 +974,12 @@ class Instaloader:
|
|||||||
def _get_id_filename(self, profile_name: str) -> str:
|
def _get_id_filename(self, profile_name: str) -> str:
|
||||||
if ((format_string_contains_key(self.dirname_pattern, 'profile') or
|
if ((format_string_contains_key(self.dirname_pattern, 'profile') or
|
||||||
format_string_contains_key(self.dirname_pattern, 'target'))):
|
format_string_contains_key(self.dirname_pattern, 'target'))):
|
||||||
return '{0}/id'.format(self.dirname_pattern.format(profile=profile_name.lower(),
|
return os.path.join(self.dirname_pattern.format(profile=profile_name.lower(),
|
||||||
target=profile_name.lower()))
|
target=profile_name.lower()),
|
||||||
|
'id')
|
||||||
else:
|
else:
|
||||||
return '{0}/{1}_id'.format(self.dirname_pattern.format(), profile_name.lower())
|
return os.path.join(self.dirname_pattern.format(),
|
||||||
|
'{0}_id'.format(profile_name.lower()))
|
||||||
|
|
||||||
def save_profile_id(self, profile: Profile):
|
def save_profile_id(self, profile: Profile):
|
||||||
"""
|
"""
|
||||||
@@ -1091,9 +1093,9 @@ class Instaloader:
|
|||||||
|
|
||||||
# Save metadata as JSON if desired.
|
# Save metadata as JSON if desired.
|
||||||
if self.save_metadata:
|
if self.save_metadata:
|
||||||
json_filename = '{0}/{1}_{2}'.format(self.dirname_pattern.format(profile=profile_name,
|
json_filename = os.path.join(self.dirname_pattern.format(profile=profile_name,
|
||||||
target=profile_name),
|
target=profile_name),
|
||||||
profile_name, profile.userid)
|
'{0}_{1}'.format(profile_name, profile.userid))
|
||||||
self.save_metadata_json(json_filename, profile)
|
self.save_metadata_json(json_filename, profile)
|
||||||
|
|
||||||
# Catch some errors
|
# Catch some errors
|
||||||
|
|||||||
+22
-45
@@ -69,7 +69,6 @@ class Post:
|
|||||||
self._node = node
|
self._node = node
|
||||||
self._owner_profile = owner_profile
|
self._owner_profile = owner_profile
|
||||||
self._full_metadata_dict = None # type: Optional[Dict[str, Any]]
|
self._full_metadata_dict = None # type: Optional[Dict[str, Any]]
|
||||||
self._rhx_gis_str = None # type: Optional[str]
|
|
||||||
self._location = None # type: Optional[PostLocation]
|
self._location = None # type: Optional[PostLocation]
|
||||||
self._iphone_struct_ = None
|
self._iphone_struct_ = None
|
||||||
if 'iphone_struct' in node:
|
if 'iphone_struct' in node:
|
||||||
@@ -142,9 +141,11 @@ class Post:
|
|||||||
|
|
||||||
def _obtain_metadata(self):
|
def _obtain_metadata(self):
|
||||||
if not self._full_metadata_dict:
|
if not self._full_metadata_dict:
|
||||||
pic_json = self._context.get_json("p/{0}/".format(self.shortcode), params={})
|
pic_json = self._context.graphql_query(
|
||||||
self._full_metadata_dict = pic_json['entry_data']['PostPage'][0]['graphql']['shortcode_media']
|
'2b0673e0dc4580674a88d426fe00ea90',
|
||||||
self._rhx_gis_str = pic_json.get('rhx_gis')
|
{'shortcode': self.shortcode}
|
||||||
|
)
|
||||||
|
self._full_metadata_dict = pic_json['data']['shortcode_media']
|
||||||
if self._full_metadata_dict is None:
|
if self._full_metadata_dict is None:
|
||||||
# issue #449
|
# issue #449
|
||||||
self._context.error("Fetching Post metadata failed (issue #449). "
|
self._context.error("Fetching Post metadata failed (issue #449). "
|
||||||
@@ -161,11 +162,6 @@ class Post:
|
|||||||
assert self._full_metadata_dict is not None
|
assert self._full_metadata_dict is not None
|
||||||
return self._full_metadata_dict
|
return self._full_metadata_dict
|
||||||
|
|
||||||
@property
|
|
||||||
def _rhx_gis(self) -> Optional[str]:
|
|
||||||
self._obtain_metadata()
|
|
||||||
return self._rhx_gis_str
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _iphone_struct(self) -> Dict[str, Any]:
|
def _iphone_struct(self) -> Dict[str, Any]:
|
||||||
if not self._context.is_logged_in:
|
if not self._context.is_logged_in:
|
||||||
@@ -392,7 +388,7 @@ class Post:
|
|||||||
created_at_utc=datetime.utcfromtimestamp(node['created_at']),
|
created_at_utc=datetime.utcfromtimestamp(node['created_at']),
|
||||||
text=node['text'],
|
text=node['text'],
|
||||||
owner=Profile(self._context, node['owner']),
|
owner=Profile(self._context, node['owner']),
|
||||||
likes_count=node['edge_liked_by']['count'])
|
likes_count=node.get('edge_liked_by', {}).get('count', 0))
|
||||||
|
|
||||||
def _postcommentanswers(node):
|
def _postcommentanswers(node):
|
||||||
if 'edge_threaded_comments' not in node:
|
if 'edge_threaded_comments' not in node:
|
||||||
@@ -418,14 +414,9 @@ class Post:
|
|||||||
if self.comments == 0:
|
if self.comments == 0:
|
||||||
# Avoid doing additional requests if there are no comments
|
# Avoid doing additional requests if there are no comments
|
||||||
return
|
return
|
||||||
try:
|
|
||||||
comment_edges = self._field('edge_media_to_parent_comment', 'edges')
|
comment_edges = self._field('edge_media_to_comment', 'edges')
|
||||||
answers_count = sum([edge['node']['edge_threaded_comments']['count'] for edge in comment_edges])
|
answers_count = sum([edge['node'].get('edge_threaded_comments', {}).get('count', 0) for edge in comment_edges])
|
||||||
threaded_comments_available = True
|
|
||||||
except KeyError:
|
|
||||||
comment_edges = self._field('edge_media_to_comment', 'edges')
|
|
||||||
answers_count = 0
|
|
||||||
threaded_comments_available = False
|
|
||||||
|
|
||||||
if self.comments == len(comment_edges) + answers_count:
|
if self.comments == len(comment_edges) + answers_count:
|
||||||
# If the Post's metadata already contains all parent comments, don't do GraphQL requests to obtain them
|
# If the Post's metadata already contains all parent comments, don't do GraphQL requests to obtain them
|
||||||
@@ -433,14 +424,10 @@ class Post:
|
|||||||
return
|
return
|
||||||
yield from (_postcomment(node) for node in
|
yield from (_postcomment(node) for node in
|
||||||
self._context.graphql_node_list(
|
self._context.graphql_node_list(
|
||||||
"97b41c52301f77ce508f55e66d17620e" if threaded_comments_available
|
"97b41c52301f77ce508f55e66d17620e",
|
||||||
else "f0986789a5c5d17c2400faebf16efd0d",
|
|
||||||
{'shortcode': self.shortcode},
|
{'shortcode': self.shortcode},
|
||||||
'https://www.instagram.com/p/' + self.shortcode + '/',
|
'https://www.instagram.com/p/' + self.shortcode + '/',
|
||||||
lambda d:
|
lambda d: d['data']['shortcode_media']['edge_media_to_parent_comment']))
|
||||||
d['data']['shortcode_media'][
|
|
||||||
'edge_media_to_parent_comment' if threaded_comments_available else 'edge_media_to_comment'],
|
|
||||||
self._rhx_gis))
|
|
||||||
|
|
||||||
def get_likes(self) -> Iterator['Profile']:
|
def get_likes(self) -> Iterator['Profile']:
|
||||||
"""Iterate over all likes of the post. A :class:`Profile` instance of each likee is yielded."""
|
"""Iterate over all likes of the post. A :class:`Profile` instance of each likee is yielded."""
|
||||||
@@ -455,8 +442,7 @@ class Post:
|
|||||||
yield from (Profile(self._context, node) for node in
|
yield from (Profile(self._context, node) for node in
|
||||||
self._context.graphql_node_list("1cb6ec562846122743b61e492c85999f", {'shortcode': self.shortcode},
|
self._context.graphql_node_list("1cb6ec562846122743b61e492c85999f", {'shortcode': self.shortcode},
|
||||||
'https://www.instagram.com/p/' + self.shortcode + '/',
|
'https://www.instagram.com/p/' + self.shortcode + '/',
|
||||||
lambda d: d['data']['shortcode_media']['edge_liked_by'],
|
lambda d: d['data']['shortcode_media']['edge_liked_by']))
|
||||||
self._rhx_gis))
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_sponsored(self) -> bool:
|
def is_sponsored(self) -> bool:
|
||||||
@@ -537,7 +523,6 @@ class Profile:
|
|||||||
self._has_public_story = None # type: Optional[bool]
|
self._has_public_story = None # type: Optional[bool]
|
||||||
self._node = node
|
self._node = node
|
||||||
self._has_full_metadata = False
|
self._has_full_metadata = False
|
||||||
self._rhx_gis = None
|
|
||||||
self._iphone_struct_ = None
|
self._iphone_struct_ = None
|
||||||
if 'iphone_struct' in node:
|
if 'iphone_struct' in node:
|
||||||
# if loaded from JSON with load_structure_from_file()
|
# if loaded from JSON with load_structure_from_file()
|
||||||
@@ -599,10 +584,9 @@ class Profile:
|
|||||||
def _obtain_metadata(self):
|
def _obtain_metadata(self):
|
||||||
try:
|
try:
|
||||||
if not self._has_full_metadata:
|
if not self._has_full_metadata:
|
||||||
metadata = self._context.get_json('{}/'.format(self.username), params={})
|
metadata = self._context.get_json('{}/feed/'.format(self.username), params={})
|
||||||
self._node = metadata['entry_data']['ProfilePage'][0]['graphql']['user']
|
self._node = metadata['entry_data']['ProfilePage'][0]['graphql']['user']
|
||||||
self._has_full_metadata = True
|
self._has_full_metadata = True
|
||||||
self._rhx_gis = metadata.get('rhx_gis')
|
|
||||||
except (QueryReturnedNotFoundException, KeyError) as err:
|
except (QueryReturnedNotFoundException, KeyError) as err:
|
||||||
top_search_results = TopSearchResults(self._context, self.username)
|
top_search_results = TopSearchResults(self._context, self.username)
|
||||||
similar_profiles = [profile.username for profile in top_search_results.get_profiles()]
|
similar_profiles = [profile.username for profile in top_search_results.get_profiles()]
|
||||||
@@ -735,8 +719,7 @@ class Profile:
|
|||||||
'include_reel': False, 'include_suggested_users': False,
|
'include_reel': False, 'include_suggested_users': False,
|
||||||
'include_logged_out_extras': True,
|
'include_logged_out_extras': True,
|
||||||
'include_highlight_reels': False},
|
'include_highlight_reels': False},
|
||||||
'https://www.instagram.com/{}/'.format(self.username),
|
'https://www.instagram.com/{}/'.format(self.username))
|
||||||
self._rhx_gis)
|
|
||||||
self._has_public_story = data['data']['user']['has_public_story']
|
self._has_public_story = data['data']['user']['has_public_story']
|
||||||
assert self._has_public_story is not None
|
assert self._has_public_story is not None
|
||||||
return self._has_public_story
|
return self._has_public_story
|
||||||
@@ -795,8 +778,7 @@ class Profile:
|
|||||||
{'id': self.userid},
|
{'id': self.userid},
|
||||||
'https://www.instagram.com/{0}/'.format(self.username),
|
'https://www.instagram.com/{0}/'.format(self.username),
|
||||||
lambda d: d['data']['user']['edge_owner_to_timeline_media'],
|
lambda d: d['data']['user']['edge_owner_to_timeline_media'],
|
||||||
self._rhx_gis,
|
first_data=self._metadata('edge_owner_to_timeline_media')))
|
||||||
self._metadata('edge_owner_to_timeline_media')))
|
|
||||||
|
|
||||||
def get_saved_posts(self) -> Iterator[Post]:
|
def get_saved_posts(self) -> Iterator[Post]:
|
||||||
"""Get Posts that are marked as saved by the user."""
|
"""Get Posts that are marked as saved by the user."""
|
||||||
@@ -810,8 +792,7 @@ class Profile:
|
|||||||
{'id': self.userid},
|
{'id': self.userid},
|
||||||
'https://www.instagram.com/{0}/'.format(self.username),
|
'https://www.instagram.com/{0}/'.format(self.username),
|
||||||
lambda d: d['data']['user']['edge_saved_media'],
|
lambda d: d['data']['user']['edge_saved_media'],
|
||||||
self._rhx_gis,
|
first_data=self._metadata('edge_saved_media')))
|
||||||
self._metadata('edge_saved_media')))
|
|
||||||
|
|
||||||
def get_tagged_posts(self) -> Iterator[Post]:
|
def get_tagged_posts(self) -> Iterator[Post]:
|
||||||
"""Retrieve all posts where a profile is tagged.
|
"""Retrieve all posts where a profile is tagged.
|
||||||
@@ -822,8 +803,7 @@ class Profile:
|
|||||||
self._context.graphql_node_list("e31a871f7301132ceaab56507a66bbb7",
|
self._context.graphql_node_list("e31a871f7301132ceaab56507a66bbb7",
|
||||||
{'id': self.userid},
|
{'id': self.userid},
|
||||||
'https://www.instagram.com/{0}/'.format(self.username),
|
'https://www.instagram.com/{0}/'.format(self.username),
|
||||||
lambda d: d['data']['user']['edge_user_to_photos_of_you'],
|
lambda d: d['data']['user']['edge_user_to_photos_of_you']))
|
||||||
self._rhx_gis))
|
|
||||||
|
|
||||||
def get_igtv_posts(self) -> Iterator[Post]:
|
def get_igtv_posts(self) -> Iterator[Post]:
|
||||||
"""Retrieve all IGTV posts.
|
"""Retrieve all IGTV posts.
|
||||||
@@ -835,8 +815,7 @@ class Profile:
|
|||||||
{'id': self.userid},
|
{'id': self.userid},
|
||||||
'https://www.instagram.com/{0}/channel/'.format(self.username),
|
'https://www.instagram.com/{0}/channel/'.format(self.username),
|
||||||
lambda d: d['data']['user']['edge_felix_video_timeline'],
|
lambda d: d['data']['user']['edge_felix_video_timeline'],
|
||||||
self._rhx_gis,
|
first_data=self._metadata('edge_felix_video_timeline')))
|
||||||
self._metadata('edge_felix_video_timeline')))
|
|
||||||
|
|
||||||
def get_followers(self) -> Iterator['Profile']:
|
def get_followers(self) -> Iterator['Profile']:
|
||||||
"""
|
"""
|
||||||
@@ -850,8 +829,7 @@ class Profile:
|
|||||||
self._context.graphql_node_list("37479f2b8209594dde7facb0d904896a",
|
self._context.graphql_node_list("37479f2b8209594dde7facb0d904896a",
|
||||||
{'id': str(self.userid)},
|
{'id': str(self.userid)},
|
||||||
'https://www.instagram.com/' + self.username + '/',
|
'https://www.instagram.com/' + self.username + '/',
|
||||||
lambda d: d['data']['user']['edge_followed_by'],
|
lambda d: d['data']['user']['edge_followed_by']))
|
||||||
self._rhx_gis))
|
|
||||||
|
|
||||||
def get_followees(self) -> Iterator['Profile']:
|
def get_followees(self) -> Iterator['Profile']:
|
||||||
"""
|
"""
|
||||||
@@ -865,8 +843,7 @@ class Profile:
|
|||||||
self._context.graphql_node_list("58712303d941c6855d4e888c5f0cd22f",
|
self._context.graphql_node_list("58712303d941c6855d4e888c5f0cd22f",
|
||||||
{'id': str(self.userid)},
|
{'id': str(self.userid)},
|
||||||
'https://www.instagram.com/' + self.username + '/',
|
'https://www.instagram.com/' + self.username + '/',
|
||||||
lambda d: d['data']['user']['edge_follow'],
|
lambda d: d['data']['user']['edge_follow']))
|
||||||
self._rhx_gis))
|
|
||||||
|
|
||||||
def get_similar_accounts(self) -> Iterator['Profile']:
|
def get_similar_accounts(self) -> Iterator['Profile']:
|
||||||
"""
|
"""
|
||||||
@@ -881,8 +858,8 @@ class Profile:
|
|||||||
yield from (Profile(self._context, edge["node"]) for edge in
|
yield from (Profile(self._context, edge["node"]) for edge in
|
||||||
self._context.graphql_query("ad99dd9d3646cc3c0dda65debcd266a7",
|
self._context.graphql_query("ad99dd9d3646cc3c0dda65debcd266a7",
|
||||||
{"user_id": str(self.userid), "include_chaining": True},
|
{"user_id": str(self.userid), "include_chaining": True},
|
||||||
"https://www.instagram.com/{0}/".format(self.username),
|
"https://www.instagram.com/{0}/"
|
||||||
self._rhx_gis)["data"]["user"]["edge_chaining"]["edges"])
|
.format(self.username))["data"]["user"]["edge_chaining"]["edges"])
|
||||||
|
|
||||||
|
|
||||||
class StoryItem:
|
class StoryItem:
|
||||||
|
|||||||
Reference in New Issue
Block a user