Compare commits

...

12 Commits

Author SHA1 Message Date
Alexander Graf
39566735f7 Release of version 4.10.2
Some checks failed
Documentation / Documentation (push) Has been cancelled
PyLint and MyPy / PyLint and MyPy (3.10) (push) Has been cancelled
PyLint and MyPy / PyLint and MyPy (3.8) (push) Has been cancelled
PyLint and MyPy / PyLint and MyPy (3.9) (push) Has been cancelled
Windows EXE build / Windows EXE build (push) Has been cancelled
Mark stale issues and pull requests / Mark stale issues and pull requests (push) Has been cancelled
2023-12-09 17:59:08 +01:00
Alexander Graf
fff25d6f4a Remove unnecessary do_sleep() 2023-12-09 17:58:40 +01:00
LostXOR
b52547ea1c
Fix 403 during login due to incorrect login endpoint and CSRF token location (#2133) 2023-12-09 17:56:21 +01:00
Alexander Graf
51a16a433e Release of version 4.10.1 2023-10-16 08:27:46 +02:00
Alexander Graf
6145c5b383 Temporarily disable lint check on python 3.11
It fails due to a problem with installing dependencies, which is already
fixed in a3dc75c04e873f560761f450df9344d139037a23.
2023-10-16 08:26:10 +02:00
LostXOR
8bc35cf3a1
Fixed error when trying to log in due to changes by Instagram (#2088) 2023-10-16 08:23:25 +02:00
sczerniawski
e48251e75e
Fix crash related to --latest-stamps (#2056) 2023-09-18 08:15:35 +02:00
Alexander Graf
ebdc5f1557 Release of Version 4.10 2023-07-10 07:18:05 +02:00
Alexander Graf
4c6a184390 Merge branch 'master' into upcoming/v4.10 2023-07-10 07:15:36 +02:00
ybenel
ed9ca62a48
Get Followed Hashtags (#1967) 2023-05-24 07:32:47 +02:00
Alexander Graf
b40a5aa243 resumable_iterator: catch EOFError when loading resume file
Fixes #1905.
2023-05-02 09:48:02 +02:00
Alexander Graf
cc8c9ff883 README: Add new sponsor 2023-04-25 07:33:37 +02:00
7 changed files with 36 additions and 26 deletions

View File

@ -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

View File

@ -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

View File

@ -1,7 +1,7 @@
"""Download pictures (or videos) along with their captions and other metadata from Instagram.""" """Download pictures (or videos) along with their captions and other metadata from Instagram."""
__version__ = '4.10b1' __version__ = '4.10.2'
try: try:

View File

@ -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":

View File

@ -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)

View File

@ -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

View File

@ -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.