Compare commits
12 Commits
99c3960ad4
...
39566735f7
Author | SHA1 | Date | |
---|---|---|---|
![]() |
39566735f7 | ||
![]() |
fff25d6f4a | ||
![]() |
b52547ea1c | ||
![]() |
51a16a433e | ||
![]() |
6145c5b383 | ||
![]() |
8bc35cf3a1 | ||
![]() |
e48251e75e | ||
![]() |
ebdc5f1557 | ||
![]() |
4c6a184390 | ||
![]() |
ed9ca62a48 | ||
![]() |
b40a5aa243 | ||
![]() |
cc8c9ff883 |
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@ -9,7 +9,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.8", "3.9", "3.10", "3.11"]
|
python-version: ["3.8", "3.9", "3.10"]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Instaloader Repository
|
- name: Checkout Instaloader Repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
@ -124,6 +124,7 @@ Supporters
|
|||||||
.. current-sponsors-start
|
.. current-sponsors-start
|
||||||
|
|
||||||
| Instaloader is proudly sponsored by
|
| Instaloader is proudly sponsored by
|
||||||
|
| `@rocketapi-io <https://github.com/rocketapi-io>`__
|
||||||
| `@socialmethod <https://github.com/socialmethod>`__
|
| `@socialmethod <https://github.com/socialmethod>`__
|
||||||
|
|
||||||
See `Alex' GitHub Sponsors <https://github.com/sponsors/aandergr>`__ page for
|
See `Alex' GitHub Sponsors <https://github.com/sponsors/aandergr>`__ page for
|
||||||
|
@ -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.10b1'
|
__version__ = '4.10.2'
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -3,7 +3,6 @@ import json
|
|||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
import random
|
import random
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
@ -256,18 +255,23 @@ class InstaloaderContext:
|
|||||||
# Override default timeout behavior.
|
# Override default timeout behavior.
|
||||||
# Need to silence mypy bug for this. See: https://github.com/python/mypy/issues/2427
|
# Need to silence mypy bug for this. See: https://github.com/python/mypy/issues/2427
|
||||||
session.request = partial(session.request, timeout=self.request_timeout) # type: ignore
|
session.request = partial(session.request, timeout=self.request_timeout) # type: ignore
|
||||||
csrf_json = self.get_json('accounts/login/', {}, session=session)
|
|
||||||
csrf_token = csrf_json['config']['csrf_token']
|
# Make a request to Instagram's root URL, which will set the session's csrftoken cookie
|
||||||
|
# Not using self.get_json() here, because we need to access the cookie
|
||||||
|
session.get('https://www.instagram.com/')
|
||||||
|
# Add session's csrftoken cookie to session headers
|
||||||
|
csrf_token = session.cookies.get_dict()['csrftoken']
|
||||||
session.headers.update({'X-CSRFToken': csrf_token})
|
session.headers.update({'X-CSRFToken': csrf_token})
|
||||||
# Not using self.get_json() here, because we need to access csrftoken cookie
|
|
||||||
self.do_sleep()
|
self.do_sleep()
|
||||||
# Workaround credits to pgrimaud.
|
# Workaround credits to pgrimaud.
|
||||||
# See: https://github.com/pgrimaud/instagram-user-feed/commit/96ad4cf54d1ad331b337f325c73e664999a6d066
|
# See: https://github.com/pgrimaud/instagram-user-feed/commit/96ad4cf54d1ad331b337f325c73e664999a6d066
|
||||||
enc_password = '#PWD_INSTAGRAM_BROWSER:0:{}:{}'.format(int(datetime.now().timestamp()), passwd)
|
enc_password = '#PWD_INSTAGRAM_BROWSER:0:{}:{}'.format(int(datetime.now().timestamp()), passwd)
|
||||||
login = session.post('https://www.instagram.com/accounts/login/ajax/',
|
login = session.post('https://www.instagram.com/api/v1/web/accounts/login/ajax/',
|
||||||
data={'enc_password': enc_password, 'username': user}, allow_redirects=True)
|
data={'enc_password': enc_password, 'username': user}, allow_redirects=True)
|
||||||
try:
|
try:
|
||||||
resp_json = login.json()
|
resp_json = login.json()
|
||||||
|
|
||||||
except json.decoder.JSONDecodeError as err:
|
except json.decoder.JSONDecodeError as err:
|
||||||
raise ConnectionException(
|
raise ConnectionException(
|
||||||
"Login error: JSON decode fail, {} - {}.".format(login.status_code, login.reason)
|
"Login error: JSON decode fail, {} - {}.".format(login.status_code, login.reason)
|
||||||
@ -403,23 +407,6 @@ class InstaloaderContext:
|
|||||||
raise TooManyRequestsException("429 Too Many Requests")
|
raise TooManyRequestsException("429 Too Many Requests")
|
||||||
if resp.status_code != 200:
|
if resp.status_code != 200:
|
||||||
raise ConnectionException("HTTP error code {}.".format(resp.status_code))
|
raise ConnectionException("HTTP error code {}.".format(resp.status_code))
|
||||||
is_html_query = not is_graphql_query and not "__a" in params and host == "www.instagram.com"
|
|
||||||
if is_html_query:
|
|
||||||
match = re.search(r'window\._sharedData = (.*);</script>', resp.text)
|
|
||||||
if match is None:
|
|
||||||
raise QueryReturnedNotFoundException("Could not find \"window._sharedData\" in html response.")
|
|
||||||
resp_json = json.loads(match.group(1))
|
|
||||||
entry_data = resp_json.get('entry_data')
|
|
||||||
post_or_profile_page = list(entry_data.values())[0] if entry_data is not None else None
|
|
||||||
if post_or_profile_page is None:
|
|
||||||
raise ConnectionException("\"window._sharedData\" does not contain required keys.")
|
|
||||||
# If GraphQL data is missing in `window._sharedData`, search for it in `__additionalDataLoaded`.
|
|
||||||
if 'graphql' not in post_or_profile_page[0]:
|
|
||||||
match = re.search(r'window\.__additionalDataLoaded\(.*?({.*"graphql":.*})\);</script>',
|
|
||||||
resp.text)
|
|
||||||
if match is not None:
|
|
||||||
post_or_profile_page[0]['graphql'] = json.loads(match.group(1))['graphql']
|
|
||||||
return resp_json
|
|
||||||
else:
|
else:
|
||||||
resp_json = resp.json()
|
resp_json = resp.json()
|
||||||
if 'status' in resp_json and resp_json['status'] != "ok":
|
if 'status' in resp_json and resp_json['status'] != "ok":
|
||||||
|
@ -27,7 +27,8 @@ class LatestStamps:
|
|||||||
self.data.read(latest_stamps_file)
|
self.data.read(latest_stamps_file)
|
||||||
|
|
||||||
def _save(self):
|
def _save(self):
|
||||||
makedirs(dirname(self.file), exist_ok=True)
|
if dn := dirname(self.file):
|
||||||
|
makedirs(dn, exist_ok=True)
|
||||||
with open(self.file, 'w') as f:
|
with open(self.file, 'w') as f:
|
||||||
self.data.write(f)
|
self.data.write(f)
|
||||||
|
|
||||||
|
@ -289,7 +289,7 @@ def resumable_iteration(context: InstaloaderContext,
|
|||||||
is_resuming = True
|
is_resuming = True
|
||||||
start_index = iterator.total_index
|
start_index = iterator.total_index
|
||||||
context.log("Resuming from {}.".format(resume_file_path))
|
context.log("Resuming from {}.".format(resume_file_path))
|
||||||
except (InvalidArgumentException, LZMAError, json.decoder.JSONDecodeError) as exc:
|
except (InvalidArgumentException, LZMAError, json.decoder.JSONDecodeError, EOFError) as exc:
|
||||||
context.error("Warning: Not resuming from {}: {}".format(resume_file_path, exc))
|
context.error("Warning: Not resuming from {}: {}".format(resume_file_path, exc))
|
||||||
try:
|
try:
|
||||||
yield is_resuming, start_index
|
yield is_resuming, start_index
|
||||||
|
@ -1097,6 +1097,27 @@ class Profile:
|
|||||||
def _make_is_newest_checker() -> Callable[[Post, Optional[Post]], bool]:
|
def _make_is_newest_checker() -> Callable[[Post, Optional[Post]], bool]:
|
||||||
return lambda post, first: first is None or post.date_local > first.date_local
|
return lambda post, first: first is None or post.date_local > first.date_local
|
||||||
|
|
||||||
|
def get_followed_hashtags(self) -> NodeIterator['Hashtag']:
|
||||||
|
"""
|
||||||
|
Retrieve list of hashtags followed by given profile.
|
||||||
|
To use this, one needs to be logged in and private profiles has to be followed.
|
||||||
|
|
||||||
|
:rtype: NodeIterator[Hashtag]
|
||||||
|
|
||||||
|
.. versionadded:: 4.10
|
||||||
|
"""
|
||||||
|
if not self._context.is_logged_in:
|
||||||
|
raise LoginRequiredException("--login required to get a profile's followers.")
|
||||||
|
self._obtain_metadata()
|
||||||
|
return NodeIterator(
|
||||||
|
self._context,
|
||||||
|
'e6306cc3dbe69d6a82ef8b5f8654c50b',
|
||||||
|
lambda d: d["data"]["user"]["edge_following_hashtag"],
|
||||||
|
lambda n: Hashtag(self._context, n),
|
||||||
|
{'id': str(self.userid)},
|
||||||
|
'https://www.instagram.com/{0}/'.format(self.username),
|
||||||
|
)
|
||||||
|
|
||||||
def get_followers(self) -> NodeIterator['Profile']:
|
def get_followers(self) -> NodeIterator['Profile']:
|
||||||
"""
|
"""
|
||||||
Retrieve list of followers of given profile.
|
Retrieve list of followers of given profile.
|
||||||
|
Loading…
Reference in New Issue
Block a user