mirror of
https://github.com/instaloader/instaloader.git
synced 2026-10-16 21:52:17 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6343e9a69d | ||
|
|
06574eb428 | ||
|
|
815ba83d77 | ||
|
|
470fc0d905 | ||
|
|
0300d8a4f1 | ||
|
|
f09bd51ed4 | ||
|
|
2c116683c2 | ||
|
|
054fa47245 | ||
|
|
1f8ebe09b4 | ||
|
|
87dfdff335 | ||
|
|
631503d463 | ||
|
|
43c9d32b22 | ||
|
|
1ad9111753 | ||
|
|
327fcfd8e8 | ||
|
|
ae39ab9893 | ||
|
|
0372f7c8e8 | ||
|
|
aaeaaa0e22 | ||
|
|
35bd8357d1 | ||
|
|
9cdf679fc1 | ||
|
|
94d599db61 | ||
|
|
a0e3e8db0d | ||
|
|
26c9f71132 | ||
|
|
210719af56 | ||
|
|
8c1129e556 | ||
|
|
c200031be6 | ||
|
|
a0d2ec95b9 |
@@ -6,13 +6,15 @@ labels: "feature suggestion"
|
||||
---
|
||||
|
||||
**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**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives 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 pull request?**
|
||||
Yes / No
|
||||
@@ -21,4 +23,9 @@ Yes / No
|
||||
**Additional context**
|
||||
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.
|
||||
-->
|
||||
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
custom: "https://instaloader.github.io/contributing.html#donations"
|
||||
github: [aandergr]
|
||||
custom: "https://instaloader.github.io/sponsors.html"
|
||||
|
||||
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.8, 3.9]
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
steps:
|
||||
- name: Checkout Instaloader Repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -30,5 +30,5 @@ jobs:
|
||||
stale-pr-label: 'stale'
|
||||
exempt-issue-label: 'leave open'
|
||||
exempt-pr-label: 'leave open'
|
||||
days-before-stale: 90
|
||||
days-before-stale: 135
|
||||
days-before-close: 14
|
||||
|
||||
@@ -10,9 +10,9 @@ jobs:
|
||||
- name: Checkout Instaloader repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: "3.9"
|
||||
architecture: x64
|
||||
- name: Get the tagged version
|
||||
id: get_version
|
||||
|
||||
+28
-18
@@ -114,6 +114,34 @@ already have a valid session cookie file.
|
||||
|
||||
`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
|
||||
|
||||
See `Alex' GitHub Sponsors <https://github.com/sponsors/aandergr>`__ page for
|
||||
how you can sponsor the development of Instaloader!
|
||||
|
||||
.. 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
|
||||
----------
|
||||
@@ -126,21 +154,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.
|
||||
|
||||
.. 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
|
||||
|
||||
@@ -45,7 +45,7 @@ with open('__main__.py', 'w+') as f:
|
||||
|
||||
# install dependencies and invoke PyInstaller
|
||||
commands = ["pip install pipenv==2020.11.15",
|
||||
"pipenv sync --dev",
|
||||
f"pipenv --python {sys.version_info.major}.{sys.version_info.minor} sync --dev",
|
||||
"pipenv run pyinstaller --log-level=DEBUG instaloader.spec"]
|
||||
|
||||
for command in commands:
|
||||
|
||||
+5
-17
@@ -132,21 +132,9 @@ details. If you feel the need to do so, please create a "feature suggestion".
|
||||
be implemented and the underlying problem could be solved. Also **describe
|
||||
alternatives** that you have considered.
|
||||
|
||||
Donations
|
||||
---------
|
||||
Sponsoring
|
||||
----------
|
||||
|
||||
.. donations-start
|
||||
|
||||
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.me/aandergr <https://www.paypal.me/aandergr>`__
|
||||
| BTC: 1Nst4LoadeYzrKjJ1DX9CpbLXBYE9RKLwY
|
||||
|
||||
.. donations-end
|
||||
|
||||
.. (Discussion in :issue:`130`)
|
||||
.. include:: sponsors.rst
|
||||
:start-after: donations-start
|
||||
:end-before: donations-end
|
||||
|
||||
+11
-8
@@ -78,13 +78,6 @@ Useful Links
|
||||
- `Issue Tracker / Bug Tracker <https://github.com/instaloader/instaloader/issues>`__
|
||||
- `Version History <https://github.com/instaloader/instaloader/releases>`__
|
||||
|
||||
Disclaimer
|
||||
----------
|
||||
|
||||
.. include:: ../README.rst
|
||||
:start-after: disclaimer-start
|
||||
:end-before: disclaimer-end
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
@@ -92,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
|
||||
become an even greater tool.
|
||||
|
||||
.. include:: contributing.rst
|
||||
Supporters
|
||||
----------
|
||||
|
||||
.. include:: sponsors.rst
|
||||
:start-after: donations-start
|
||||
:end-before: donations-end
|
||||
|
||||
Disclaimer
|
||||
----------
|
||||
|
||||
.. include:: ../README.rst
|
||||
:start-after: disclaimer-start
|
||||
:end-before: disclaimer-end
|
||||
|
||||
..
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
|
||||
@@ -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
|
||||
@@ -62,7 +62,8 @@ Two-Factor-Authentication. It also supports handling the *checkpoint challenge*,
|
||||
issued when Instagram suspects authentication activity on your account, by
|
||||
pointing the user to an URL to be opened in a browser.
|
||||
|
||||
Nevertheless, in :issue:`92` and :issue:`615` users reported problems with
|
||||
Nevertheless, in :issue:`92`, :issue:`615`, :issue:`1150` and :issue:`1217`,
|
||||
users reported problems with
|
||||
logging in. We recommend to always keep the session file which Instaloader
|
||||
creates when using :option:`--login`. If a session file is present,
|
||||
:option:`--login` does not make use of the failure-prone login procedure.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Download pictures (or videos) along with their captions and other metadata from Instagram."""
|
||||
|
||||
|
||||
__version__ = '4.8rc2'
|
||||
__version__ = '4.8.2'
|
||||
|
||||
|
||||
try:
|
||||
|
||||
@@ -93,6 +93,7 @@ def _main(instaloader: Instaloader, targetlist: List[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}.")
|
||||
# Login, if desired
|
||||
if username is not None:
|
||||
if not re.match(r"^[A-Za-z0-9._]+$", username):
|
||||
|
||||
+21
-16
@@ -453,9 +453,12 @@ class Instaloader:
|
||||
def save_location(self, filename: str, location: PostLocation, mtime: datetime) -> None:
|
||||
"""Save post location name and Google Maps link."""
|
||||
filename += '_location.txt'
|
||||
location_string = (location.name + "\n" +
|
||||
"https://maps.google.com/maps?q={0},{1}&ll={0},{1}\n".format(location.lat,
|
||||
location.lng))
|
||||
if location.lat is not None and location.lng is not None:
|
||||
location_string = (location.name + "\n" +
|
||||
"https://maps.google.com/maps?q={0},{1}&ll={0},{1}\n".format(location.lat,
|
||||
location.lng))
|
||||
else:
|
||||
location_string = location.name
|
||||
with open(filename, 'wb') as text_file:
|
||||
with BytesIO(location_string.encode()) as bio:
|
||||
shutil.copyfileobj(cast(IO, bio), text_file)
|
||||
@@ -788,6 +791,7 @@ class Instaloader:
|
||||
|
||||
if not userids:
|
||||
self.context.log("Retrieving all visible stories...")
|
||||
profile_count = None
|
||||
else:
|
||||
userids = [p if isinstance(p, int) else p.userid for p in userids]
|
||||
profile_count = len(userids)
|
||||
@@ -807,7 +811,7 @@ class Instaloader:
|
||||
last_scraped = latest_stamps.get_last_story_timestamp(name)
|
||||
scraped_timestamp = datetime.now().astimezone()
|
||||
for item in user_story.get_items():
|
||||
if last_scraped is not None and item.date_utc.replace(tzinfo=timezone.utc) <= last_scraped:
|
||||
if latest_stamps is not None and item.date_utc.replace(tzinfo=timezone.utc) <= last_scraped:
|
||||
break
|
||||
if storyitem_filter is not None and not storyitem_filter(item):
|
||||
self.context.log("<{} skipped>".format(item), flush=True)
|
||||
@@ -1203,14 +1207,14 @@ class Instaloader:
|
||||
if latest_stamps is not None:
|
||||
last_scraped = latest_stamps.get_last_tagged_timestamp(profile.username)
|
||||
posts_takewhile = lambda p: p.date_utc.replace(tzinfo=timezone.utc) > last_scraped
|
||||
scraped_timestamp = datetime.now().astimezone()
|
||||
self.posts_download_loop(profile.get_tagged_posts(),
|
||||
tagged_posts = profile.get_tagged_posts()
|
||||
self.posts_download_loop(tagged_posts,
|
||||
target if target
|
||||
else (Path(_PostPathFormatter.sanitize_path(profile.username)) /
|
||||
_PostPathFormatter.sanitize_path(':tagged')),
|
||||
fast_update, post_filter, takewhile=posts_takewhile)
|
||||
if latest_stamps is not None:
|
||||
latest_stamps.set_last_tagged_timestamp(profile.username, scraped_timestamp)
|
||||
if latest_stamps is not None and tagged_posts.first_item is not None:
|
||||
latest_stamps.set_last_tagged_timestamp(profile.username, tagged_posts.first_item.date_local.astimezone())
|
||||
|
||||
def download_igtv(self, profile: Profile, fast_update: bool = False,
|
||||
post_filter: Optional[Callable[[Post], bool]] = None,
|
||||
@@ -1226,11 +1230,11 @@ class Instaloader:
|
||||
if latest_stamps is not None:
|
||||
last_scraped = latest_stamps.get_last_igtv_timestamp(profile.username)
|
||||
posts_takewhile = lambda p: p.date_utc.replace(tzinfo=timezone.utc) > last_scraped
|
||||
scraped_timestamp = datetime.now().astimezone()
|
||||
self.posts_download_loop(profile.get_igtv_posts(), profile.username, fast_update, post_filter,
|
||||
igtv_posts = profile.get_igtv_posts()
|
||||
self.posts_download_loop(igtv_posts, profile.username, fast_update, post_filter,
|
||||
total_count=profile.igtvcount, owner_profile=profile, takewhile=posts_takewhile)
|
||||
if latest_stamps is not None:
|
||||
latest_stamps.set_last_igtv_timestamp(profile.username, scraped_timestamp)
|
||||
if latest_stamps is not None and igtv_posts.first_item is not None:
|
||||
latest_stamps.set_last_igtv_timestamp(profile.username, igtv_posts.first_item.date_local.astimezone())
|
||||
|
||||
def _get_id_filename(self, profile_name: str) -> str:
|
||||
if ((format_string_contains_key(self.dirname_pattern, 'profile') or
|
||||
@@ -1424,12 +1428,13 @@ class Instaloader:
|
||||
# pylint:disable=cell-var-from-loop
|
||||
last_scraped = latest_stamps.get_last_post_timestamp(profile_name)
|
||||
posts_takewhile = lambda p: p.date_utc.replace(tzinfo=timezone.utc) > last_scraped
|
||||
scraped_timestamp = datetime.now().astimezone()
|
||||
self.posts_download_loop(profile.get_posts(), profile_name, fast_update, post_filter,
|
||||
posts_to_download = profile.get_posts()
|
||||
self.posts_download_loop(posts_to_download, profile_name, fast_update, post_filter,
|
||||
total_count=profile.mediacount, owner_profile=profile,
|
||||
takewhile=posts_takewhile)
|
||||
if latest_stamps is not None:
|
||||
latest_stamps.set_last_post_timestamp(profile_name, scraped_timestamp)
|
||||
if latest_stamps is not None and posts_to_download.first_item is not None:
|
||||
latest_stamps.set_last_post_timestamp(profile_name,
|
||||
posts_to_download.first_item.date_local.astimezone())
|
||||
|
||||
if stories and profiles:
|
||||
with self.context.error_catcher("Download stories"):
|
||||
|
||||
@@ -154,7 +154,6 @@ class InstaloaderContext:
|
||||
if empty_session_only:
|
||||
del header['Host']
|
||||
del header['Origin']
|
||||
del header['Referer']
|
||||
del header['X-Instagram-AJAX']
|
||||
del header['X-Requested-With']
|
||||
return header
|
||||
@@ -530,6 +529,28 @@ class InstaloaderContext:
|
||||
:raises ConnectionException: When download repeatedly failed."""
|
||||
self.write_raw(self.get_raw(url), filename)
|
||||
|
||||
def head(self, url: str, allow_redirects: bool = False) -> requests.Response:
|
||||
"""HEAD a URL anonymously.
|
||||
|
||||
:raises QueryReturnedNotFoundException: When the server responds with a 404.
|
||||
:raises QueryReturnedForbiddenException: When the server responds with a 403.
|
||||
:raises ConnectionException: When request failed.
|
||||
|
||||
.. versionadded:: 4.7.6
|
||||
"""
|
||||
with self.get_anonymous_session() as anonymous_session:
|
||||
resp = anonymous_session.head(url, allow_redirects=allow_redirects)
|
||||
if resp.status_code == 200:
|
||||
return resp
|
||||
else:
|
||||
if resp.status_code == 403:
|
||||
# suspected invalid URL signature
|
||||
raise QueryReturnedForbiddenException("403 when accessing {}.".format(url))
|
||||
if resp.status_code == 404:
|
||||
# 404 not worth retrying.
|
||||
raise QueryReturnedNotFoundException("404 when accessing {}.".format(url))
|
||||
raise ConnectionException("HTTP error code {}.".format(resp.status_code))
|
||||
|
||||
@property
|
||||
def root_rhx_gis(self) -> Optional[str]:
|
||||
"""rhx_gis string returned in the / query."""
|
||||
|
||||
+29
-11
@@ -17,7 +17,8 @@ FrozenNodeIterator = NamedTuple('FrozenNodeIterator',
|
||||
('context_username', Optional[str]),
|
||||
('total_index', int),
|
||||
('best_before', Optional[float]),
|
||||
('remaining_data', Optional[Dict])])
|
||||
('remaining_data', Optional[Dict]),
|
||||
('first_node', Optional[Dict])])
|
||||
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."""
|
||||
@@ -26,7 +27,7 @@ FrozenNodeIterator.total_index.__doc__ = """Number of items that have already be
|
||||
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."""
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
@@ -89,6 +90,7 @@ class NodeIterator(Iterator[T]):
|
||||
self._best_before = datetime.now() + NodeIterator._shelf_life
|
||||
else:
|
||||
self._data = self._query()
|
||||
self._first_node: Optional[Dict] = None
|
||||
|
||||
def _query(self, after: Optional[str] = None) -> Dict:
|
||||
pagination_variables = {'first': NodeIterator._graphql_page_length} # type: Dict[str, Any]
|
||||
@@ -125,17 +127,21 @@ class NodeIterator(Iterator[T]):
|
||||
except KeyboardInterrupt:
|
||||
self._page_index, self._total_index = page_index, total_index
|
||||
raise
|
||||
return self._node_wrapper(node)
|
||||
item = self._node_wrapper(node)
|
||||
if self._first_node is None:
|
||||
self._first_node = node
|
||||
return item
|
||||
if self._data['page_info']['has_next_page']:
|
||||
query_response = self._query(self._data['page_info']['end_cursor'])
|
||||
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__()
|
||||
if self._data['edges'] != query_response['edges']:
|
||||
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
|
||||
@@ -157,6 +163,15 @@ class NodeIterator(Iterator[T]):
|
||||
).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.
|
||||
|
||||
.. versionadded:: 4.8
|
||||
"""
|
||||
return self._node_wrapper(self._first_node) if self._first_node is not None else None
|
||||
|
||||
def freeze(self) -> FrozenNodeIterator:
|
||||
"""Freeze the iterator for later resuming."""
|
||||
remaining_data = None
|
||||
@@ -171,6 +186,7 @@ class NodeIterator(Iterator[T]):
|
||||
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,
|
||||
)
|
||||
|
||||
def thaw(self, frozen: FrozenNodeIterator) -> None:
|
||||
@@ -197,6 +213,8 @@ class NodeIterator(Iterator[T]):
|
||||
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
|
||||
|
||||
+64
-12
@@ -5,7 +5,7 @@ from base64 import b64decode, b64encode
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, Iterator, List, Optional, Union
|
||||
from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union
|
||||
|
||||
from . import __version__
|
||||
from .exceptions import *
|
||||
@@ -35,8 +35,8 @@ PostLocation.id.__doc__ = "ID number of location."
|
||||
PostLocation.name.__doc__ = "Location name."
|
||||
PostLocation.slug.__doc__ = "URL friendly variant of location name."
|
||||
PostLocation.has_public_page.__doc__ = "Whether location has a public page."
|
||||
PostLocation.lat.__doc__ = "Latitude (:class:`float`)."
|
||||
PostLocation.lng.__doc__ = "Longitude (:class:`float`)."
|
||||
PostLocation.lat.__doc__ = "Latitude (:class:`float` or None)."
|
||||
PostLocation.lng.__doc__ = "Longitude (:class:`float` or None)."
|
||||
|
||||
|
||||
class Post:
|
||||
@@ -377,13 +377,35 @@ class Post:
|
||||
def video_url(self) -> Optional[str]:
|
||||
"""URL of the video, or None."""
|
||||
if self.is_video:
|
||||
version_urls = []
|
||||
try:
|
||||
version_urls.append(self._field('video_url'))
|
||||
except (InstaloaderException, KeyError, IndexError) as err:
|
||||
self._context.error(f"Warning: Unable to fetch video from graphql of {self}: {err}")
|
||||
if self._context.iphone_support and self._context.is_logged_in:
|
||||
try:
|
||||
url = self._iphone_struct['video_versions'][0]['url']
|
||||
return url
|
||||
version_urls.extend(version['url'] for version in self._iphone_struct['video_versions'])
|
||||
except (InstaloaderException, KeyError, IndexError) as err:
|
||||
self._context.error('{} Unable to fetch high quality video version of {}.'.format(err, self))
|
||||
return self._field('video_url')
|
||||
self._context.error(f"Unable to fetch high-quality video version of {self}: {err}")
|
||||
version_urls = list(dict.fromkeys(version_urls))
|
||||
if len(version_urls) == 0:
|
||||
return None
|
||||
if len(version_urls) == 1:
|
||||
return version_urls[0]
|
||||
url_candidates: List[Tuple[int, str]] = []
|
||||
for idx, version_url in enumerate(version_urls):
|
||||
try:
|
||||
url_candidates.append((
|
||||
int(self._context.head(version_url, allow_redirects=True).headers.get('Content-Length', 0)),
|
||||
version_url
|
||||
))
|
||||
except (InstaloaderException, KeyError, IndexError) as err:
|
||||
self._context.error(f"Video URL candidate {idx+1}/{len(version_urls)} for {self}: {err}")
|
||||
if not url_candidates:
|
||||
# All candidates fail: Fallback to default URL and handle errors later at the actual download attempt
|
||||
return version_urls[0]
|
||||
url_candidates.sort()
|
||||
return url_candidates[-1][1]
|
||||
return None
|
||||
|
||||
@property
|
||||
@@ -556,10 +578,10 @@ class Post:
|
||||
return None
|
||||
location_id = int(loc['id'])
|
||||
if any(k not in loc for k in ('name', 'slug', 'has_public_page', 'lat', 'lng')):
|
||||
loc = self._context.get_json("explore/locations/{0}/".format(location_id),
|
||||
params={'__a': 1})['graphql']['location']
|
||||
loc.update(self._context.get_json("explore/locations/{0}/".format(location_id),
|
||||
params={'__a': 1})['native_location_data']['location_info'])
|
||||
self._location = PostLocation(location_id, loc['name'], loc['slug'], loc['has_public_page'],
|
||||
loc['lat'], loc['lng'])
|
||||
loc.get('lat'), loc.get('lng'))
|
||||
return self._location
|
||||
|
||||
|
||||
@@ -1110,7 +1132,35 @@ class StoryItem:
|
||||
def video_url(self) -> Optional[str]:
|
||||
"""URL of the video, or None."""
|
||||
if self.is_video:
|
||||
return self._node['video_resources'][-1]['src']
|
||||
version_urls = []
|
||||
try:
|
||||
version_urls.append(self._node['video_resources'][-1]['src'])
|
||||
except (InstaloaderException, KeyError, IndexError) as err:
|
||||
self._context.error(f"Warning: Unable to fetch video from graphql of {self}: {err}")
|
||||
if self._context.iphone_support and self._context.is_logged_in:
|
||||
try:
|
||||
version_urls.extend(version['url'] for version in self._iphone_struct['video_versions'])
|
||||
except (InstaloaderException, KeyError, IndexError) as err:
|
||||
self._context.error(f"Unable to fetch high-quality video version of {self}: {err}")
|
||||
version_urls = list(dict.fromkeys(version_urls))
|
||||
if len(version_urls) == 0:
|
||||
return None
|
||||
if len(version_urls) == 1:
|
||||
return version_urls[0]
|
||||
url_candidates: List[Tuple[int, str]] = []
|
||||
for idx, version_url in enumerate(version_urls):
|
||||
try:
|
||||
url_candidates.append((
|
||||
int(self._context.head(version_url, allow_redirects=True).headers.get('Content-Length', 0)),
|
||||
version_url
|
||||
))
|
||||
except (InstaloaderException, KeyError, IndexError) as err:
|
||||
self._context.error(f"Video URL candidate {idx+1}/{len(version_urls)} for {self}: {err}")
|
||||
if not url_candidates:
|
||||
# All candidates fail: Fallback to default URL and handle errors later at the actual download attempt
|
||||
return version_urls[0]
|
||||
url_candidates.sort()
|
||||
return url_candidates[-1][1]
|
||||
return None
|
||||
|
||||
|
||||
@@ -1509,7 +1559,7 @@ class TopSearchResults:
|
||||
place = location.get('place', {})
|
||||
slug = place.get('slug')
|
||||
loc = place.get('location', {})
|
||||
yield PostLocation(int(loc['pk']), loc['name'], slug, None, loc['lat'], loc['lng'])
|
||||
yield PostLocation(int(loc['pk']), loc['name'], slug, None, loc.get('lat'), loc.get('lng'))
|
||||
|
||||
def get_hashtag_strings(self) -> Iterator[str]:
|
||||
"""
|
||||
@@ -1643,6 +1693,8 @@ def load_structure(context: InstaloaderContext, json_structure: dict) -> JsonExp
|
||||
elif node_type == "Hashtag":
|
||||
return Hashtag(context, json_structure['node'])
|
||||
elif node_type == "FrozenNodeIterator":
|
||||
if not 'first_node' in json_structure['node']:
|
||||
json_structure['node']['first_node'] = None
|
||||
return FrozenNodeIterator(**json_structure['node'])
|
||||
elif 'shortcode' in json_structure:
|
||||
# Post JSON created with Instaloader v3
|
||||
|
||||
Reference in New Issue
Block a user