Compare commits

...
8 Commits
Author SHA1 Message Date
Alexander Graf 520b69d6ef Release of version 4.15 2025-11-08 22:27:37 +01:00
Alexander Graf 25c4daa861 Merge branch 'upcoming/v4.15' 2025-11-08 22:19:37 +01:00
Alexander Graf 7d256c36c1 Avoid using deprecated Hashtag.get_posts in codesnippets 2025-11-08 22:14:36 +01:00
Samuele MazziandAlexander Graf 64908788d3 Fix anonymous fetch of profile posts (#2533)
Co-authored-by: Alexander Graf <17130992+aandergr@users.noreply.github.com>
2025-11-08 22:07:58 +01:00
DenysandAlexander Graf b7ef306374 Abort download on {feedback,checkpoint,challenge}_required (#2612)
Handle Instagram feedback_required/checkpoint_required/challenge_required responses with a AbortDownloadException, causing the resumable iteration to be interrupted.

Co-authored-by: Alexander Graf <17130992+aandergr@users.noreply.github.com>
2025-11-08 18:10:47 +01:00
Alexander Graf 813621026c Update default user agent 2025-11-08 17:36:41 +01:00
Alexander Graf 44bd4d173b Update dependency versions used in CI 2025-11-08 17:33:16 +01:00
Naitri Doshi 2393a1a7a4 Add Post.video_play_count property (#2595) 2025-10-23 11:02:21 +00:00
12 changed files with 604 additions and 454 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
- name: "Setup Python" - name: "Setup Python"
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: 3.12 python-version: 3.14
cache: 'pipenv' cache: 'pipenv'
- name: "Install Dependencies" - name: "Install Dependencies"
run: | run: |
+2 -1
View File
@@ -9,7 +9,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
steps: steps:
- name: Checkout Instaloader Repository - name: Checkout Instaloader Repository
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -23,6 +23,7 @@ jobs:
python -m pip install pipenv==2025.0.4 python -m pip install pipenv==2025.0.4
pipenv --python `python --version | grep -Eo '3\.[0-9]+'` sync --dev pipenv --python `python --version | grep -Eo '3\.[0-9]+'` sync --dev
- name: PyLint - name: PyLint
if: matrix.python-version != '3.9'
run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run pylint instaloader run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run pylint instaloader
- name: MyPy - name: MyPy
run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run mypy -m instaloader run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run mypy -m instaloader
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
- name: "Setup Python" - name: "Setup Python"
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: 3.12 python-version: 3.14
- name: "Get the tagged version" - name: "Get the tagged version"
id: get_version id: get_version
env: env:
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: "3.13" python-version: "3.14"
cache: 'pipenv' cache: 'pipenv'
architecture: x64 architecture: x64
- name: Get the tagged version - name: Get the tagged version
Generated
+545 -433
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -312,7 +312,7 @@ How to Download
.. 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/138 on Linux. Chrome/142 on Linux.
.. option:: --max-connection-attempts N .. option:: --max-connection-attempts N
+1 -1
View File
@@ -2,7 +2,7 @@ import instaloader
L = instaloader.Instaloader() L = instaloader.Instaloader()
posts = instaloader.Hashtag.from_name(L.context, 'urbanphotography').get_posts() posts = instaloader.Hashtag.from_name(L.context, 'urbanphotography').get_posts_resumable()
users = set() users = set()
@@ -3,7 +3,7 @@ import instaloader
L = instaloader.Instaloader() L = instaloader.Instaloader()
posts = instaloader.Hashtag.from_name(L.context, "urbanphotography").get_posts() posts = instaloader.Hashtag.from_name(L.context, "urbanphotography").get_posts_resumable()
SINCE = datetime(2020, 5, 10) # further from today, inclusive SINCE = datetime(2020, 5, 10) # further from today, inclusive
UNTIL = datetime(2020, 5, 11) # closer to today, not inclusive UNTIL = datetime(2020, 5, 11) # closer to today, not inclusive
+1 -1
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.15a1' __version__ = '4.15'
try: try:
+14 -2
View File
@@ -32,7 +32,7 @@ def copy_session(session: requests.Session, request_timeout: Optional[float] = N
def default_user_agent() -> str: def default_user_agent() -> str:
return ('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' return ('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36') '(KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36')
def default_iphone_headers() -> Dict[str, Any]: def default_iphone_headers() -> Dict[str, Any]:
@@ -395,7 +395,10 @@ class InstaloaderContext:
:param session: Session to use, or None to use self.session :param session: Session to use, or None to use self.session
:param use_post: Use POST instead of GET to make the request :param use_post: Use POST instead of GET to make the request
:return: Decoded response dictionary :return: Decoded response dictionary
:raises QueryReturnedBadRequestException: When the server responds with a 400. :raises AbortDownloadException: When the server responds with
'feedback_required'/'checkpoint_required'/'challenge_required'
:raises QueryReturnedBadRequestException: When the server responds with a 400 (and not
'feedback_required'/'checkpoint_required'/'challenge_required').
:raises QueryReturnedNotFoundException: When the server responds with a 404. :raises QueryReturnedNotFoundException: When the server responds with a 404.
:raises ConnectionException: When query repeatedly failed. :raises ConnectionException: When query repeatedly failed.
@@ -447,6 +450,15 @@ class InstaloaderContext:
response_headers.clear() response_headers.clear()
response_headers.update(resp.headers) response_headers.update(resp.headers)
if resp.status_code == 400: if resp.status_code == 400:
with suppress(json.decoder.JSONDecodeError):
if resp.json().get("message") in [
"feedback_required",
"checkpoint_required",
"challenge_required",
]:
# Raise AbortDownloadException in case of substantial Instagram
# requirements to stop producing more requests
raise AbortDownloadException(self._response_error(resp))
raise QueryReturnedBadRequestException(self._response_error(resp)) raise QueryReturnedBadRequestException(self._response_error(resp))
if resp.status_code == 404: if resp.status_code == 404:
raise QueryReturnedNotFoundException(self._response_error(resp)) raise QueryReturnedNotFoundException(self._response_error(resp))
+35 -11
View File
@@ -602,6 +602,15 @@ class Post:
return self._field('video_view_count') return self._field('video_view_count')
return None return None
@property
def video_play_count(self) -> Optional[int]:
"""Play count of the video, or None.
.. versionadded:: 4.14.3"""
if self.is_video:
return self._field('video_play_count')
return None
@property @property
def video_duration(self) -> Optional[float]: def video_duration(self) -> Optional[float]:
"""Duration of the video in seconds, or None. """Duration of the video in seconds, or None.
@@ -1197,18 +1206,33 @@ class Profile:
:rtype: NodeIterator[Post]""" :rtype: NodeIterator[Post]"""
self._obtain_metadata() self._obtain_metadata()
logged_in = self._context.is_logged_in
return NodeIterator( return NodeIterator(
context = self._context, context=self._context,
edge_extractor = lambda d: d['data']['xdt_api__v1__feed__user_timeline_graphql_connection'], edge_extractor=(
node_wrapper = lambda n: Post.from_iphone_struct(self._context, n), (lambda d: d["data"]["xdt_api__v1__feed__user_timeline_graphql_connection"])
query_variables = {'data': { if logged_in
'count': 12, 'include_relationship_info': True, else (lambda d: d["data"]["user"]["edge_owner_to_timeline_media"])
'latest_besties_reel_media': True, 'latest_reel_media': True}, ),
'username': self.username}, node_wrapper=(
query_referer = 'https://www.instagram.com/{0}/'.format(self.username), (lambda n: Post.from_iphone_struct(self._context, n))
is_first = Profile._make_is_newest_checker(), if logged_in
doc_id = '7898261790222653', else (lambda n: Post(self._context, n, self))
query_hash = None, ),
query_variables={
"data": {
"count": 12,
"include_relationship_info": True,
"latest_besties_reel_media": True,
"latest_reel_media": True,
},
**({"username": self.username} if logged_in else {"id": self.userid}),
},
query_referer="https://www.instagram.com/{0}/".format(self.username),
is_first=Profile._make_is_newest_checker(),
doc_id="7898261790222653" if logged_in else "7950326061742207",
query_hash=None,
first_data=(None if logged_in else self._metadata("edge_owner_to_timeline_media")),
) )
def get_saved_posts(self) -> NodeIterator[Post]: def get_saved_posts(self) -> NodeIterator[Post]:
+1
View File
@@ -63,6 +63,7 @@ setup(
'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12', 'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13', 'Programming Language :: Python :: 3.13',
'Programming Language :: Python :: 3.14',
'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3 :: Only',
'Topic :: Internet', 'Topic :: Internet',
'Topic :: Multimedia :: Graphics' 'Topic :: Multimedia :: Graphics'