mirror of
https://github.com/instaloader/instaloader.git
synced 2026-11-01 04:55:32 -08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6e5e31005 | ||
|
|
d864ce08ff | ||
|
|
555c86633c | ||
|
|
21365ec668 | ||
|
|
6343e9a69d | ||
|
|
06574eb428 | ||
|
|
815ba83d77 | ||
|
|
470fc0d905 | ||
|
|
0300d8a4f1 | ||
|
|
f09bd51ed4 |
@@ -6,13 +6,15 @@ labels: "feature suggestion"
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Provide us a use case of the feature**
|
**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**
|
**Describe the solution you'd like**
|
||||||
A clear and concise description of what you want to happen.
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
**Describe alternatives you've considered**
|
||||||
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.
|
||||||
|
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?**
|
**If the feature request is accepted, would you be willing to submit a pull request?**
|
||||||
Yes / No
|
Yes / No
|
||||||
@@ -21,4 +23,9 @@ Yes / No
|
|||||||
**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 -->
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: [3.8, 3.9]
|
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
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ jobs:
|
|||||||
- name: Checkout Instaloader repository
|
- name: Checkout Instaloader repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v1
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.9
|
python-version: "3.9"
|
||||||
architecture: x64
|
architecture: x64
|
||||||
- name: Get the tagged version
|
- name: Get the tagged version
|
||||||
id: get_version
|
id: get_version
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ with open('__main__.py', 'w+') as f:
|
|||||||
|
|
||||||
# install dependencies and invoke PyInstaller
|
# install dependencies and invoke PyInstaller
|
||||||
commands = ["pip install pipenv==2020.11.15",
|
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"]
|
"pipenv run pyinstaller --log-level=DEBUG instaloader.spec"]
|
||||||
|
|
||||||
for command in commands:
|
for command in commands:
|
||||||
|
|||||||
@@ -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.8.1'
|
__version__ = '4.8.3'
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
+13
-10
@@ -453,9 +453,12 @@ class Instaloader:
|
|||||||
def save_location(self, filename: str, location: PostLocation, mtime: datetime) -> None:
|
def save_location(self, filename: str, location: PostLocation, mtime: datetime) -> None:
|
||||||
"""Save post location name and Google Maps link."""
|
"""Save post location name and Google Maps link."""
|
||||||
filename += '_location.txt'
|
filename += '_location.txt'
|
||||||
location_string = (location.name + "\n" +
|
if location.lat is not None and location.lng is not None:
|
||||||
"https://maps.google.com/maps?q={0},{1}&ll={0},{1}\n".format(location.lat,
|
location_string = (location.name + "\n" +
|
||||||
location.lng))
|
"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 open(filename, 'wb') as text_file:
|
||||||
with BytesIO(location_string.encode()) as bio:
|
with BytesIO(location_string.encode()) as bio:
|
||||||
shutil.copyfileobj(cast(IO, bio), text_file)
|
shutil.copyfileobj(cast(IO, bio), text_file)
|
||||||
@@ -808,7 +811,7 @@ class Instaloader:
|
|||||||
last_scraped = latest_stamps.get_last_story_timestamp(name)
|
last_scraped = latest_stamps.get_last_story_timestamp(name)
|
||||||
scraped_timestamp = datetime.now().astimezone()
|
scraped_timestamp = datetime.now().astimezone()
|
||||||
for item in user_story.get_items():
|
for item in user_story.get_items():
|
||||||
if latest_stamps is not None and item.date_utc.replace(tzinfo=timezone.utc) <= last_scraped:
|
if latest_stamps is not None and item.date_local <= last_scraped:
|
||||||
break
|
break
|
||||||
if storyitem_filter is not None and not storyitem_filter(item):
|
if storyitem_filter is not None and not storyitem_filter(item):
|
||||||
self.context.log("<{} skipped>".format(item), flush=True)
|
self.context.log("<{} skipped>".format(item), flush=True)
|
||||||
@@ -1203,7 +1206,7 @@ class Instaloader:
|
|||||||
posts_takewhile: Optional[Callable[[Post], bool]] = None
|
posts_takewhile: Optional[Callable[[Post], bool]] = None
|
||||||
if latest_stamps is not None:
|
if latest_stamps is not None:
|
||||||
last_scraped = latest_stamps.get_last_tagged_timestamp(profile.username)
|
last_scraped = latest_stamps.get_last_tagged_timestamp(profile.username)
|
||||||
posts_takewhile = lambda p: p.date_utc.replace(tzinfo=timezone.utc) > last_scraped
|
posts_takewhile = lambda p: p.date_local > last_scraped
|
||||||
tagged_posts = profile.get_tagged_posts()
|
tagged_posts = profile.get_tagged_posts()
|
||||||
self.posts_download_loop(tagged_posts,
|
self.posts_download_loop(tagged_posts,
|
||||||
target if target
|
target if target
|
||||||
@@ -1211,7 +1214,7 @@ class Instaloader:
|
|||||||
_PostPathFormatter.sanitize_path(':tagged')),
|
_PostPathFormatter.sanitize_path(':tagged')),
|
||||||
fast_update, post_filter, takewhile=posts_takewhile)
|
fast_update, post_filter, takewhile=posts_takewhile)
|
||||||
if latest_stamps is not None and tagged_posts.first_item is not None:
|
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())
|
latest_stamps.set_last_tagged_timestamp(profile.username, tagged_posts.first_item.date_local)
|
||||||
|
|
||||||
def download_igtv(self, profile: Profile, fast_update: bool = False,
|
def download_igtv(self, profile: Profile, fast_update: bool = False,
|
||||||
post_filter: Optional[Callable[[Post], bool]] = None,
|
post_filter: Optional[Callable[[Post], bool]] = None,
|
||||||
@@ -1226,12 +1229,12 @@ class Instaloader:
|
|||||||
posts_takewhile: Optional[Callable[[Post], bool]] = None
|
posts_takewhile: Optional[Callable[[Post], bool]] = None
|
||||||
if latest_stamps is not None:
|
if latest_stamps is not None:
|
||||||
last_scraped = latest_stamps.get_last_igtv_timestamp(profile.username)
|
last_scraped = latest_stamps.get_last_igtv_timestamp(profile.username)
|
||||||
posts_takewhile = lambda p: p.date_utc.replace(tzinfo=timezone.utc) > last_scraped
|
posts_takewhile = lambda p: p.date_local > last_scraped
|
||||||
igtv_posts = profile.get_igtv_posts()
|
igtv_posts = profile.get_igtv_posts()
|
||||||
self.posts_download_loop(igtv_posts, profile.username, fast_update, post_filter,
|
self.posts_download_loop(igtv_posts, profile.username, fast_update, post_filter,
|
||||||
total_count=profile.igtvcount, owner_profile=profile, takewhile=posts_takewhile)
|
total_count=profile.igtvcount, owner_profile=profile, takewhile=posts_takewhile)
|
||||||
if latest_stamps is not None and igtv_posts.first_item is not None:
|
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())
|
latest_stamps.set_last_igtv_timestamp(profile.username, igtv_posts.first_item.date_local)
|
||||||
|
|
||||||
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
|
||||||
@@ -1424,14 +1427,14 @@ class Instaloader:
|
|||||||
if latest_stamps is not None:
|
if latest_stamps is not None:
|
||||||
# pylint:disable=cell-var-from-loop
|
# pylint:disable=cell-var-from-loop
|
||||||
last_scraped = latest_stamps.get_last_post_timestamp(profile_name)
|
last_scraped = latest_stamps.get_last_post_timestamp(profile_name)
|
||||||
posts_takewhile = lambda p: p.date_utc.replace(tzinfo=timezone.utc) > last_scraped
|
posts_takewhile = lambda p: p.date_local > last_scraped
|
||||||
posts_to_download = profile.get_posts()
|
posts_to_download = profile.get_posts()
|
||||||
self.posts_download_loop(posts_to_download, profile_name, fast_update, post_filter,
|
self.posts_download_loop(posts_to_download, profile_name, fast_update, post_filter,
|
||||||
total_count=profile.mediacount, owner_profile=profile,
|
total_count=profile.mediacount, owner_profile=profile,
|
||||||
takewhile=posts_takewhile)
|
takewhile=posts_takewhile)
|
||||||
if latest_stamps is not None and posts_to_download.first_item is not None:
|
if latest_stamps is not None and posts_to_download.first_item is not None:
|
||||||
latest_stamps.set_last_post_timestamp(profile_name,
|
latest_stamps.set_last_post_timestamp(profile_name,
|
||||||
posts_to_download.first_item.date_local.astimezone())
|
posts_to_download.first_item.date_local)
|
||||||
|
|
||||||
if stories and profiles:
|
if stories and profiles:
|
||||||
with self.context.error_catcher("Download stories"):
|
with self.context.error_catcher("Download stories"):
|
||||||
|
|||||||
@@ -133,14 +133,15 @@ class NodeIterator(Iterator[T]):
|
|||||||
return item
|
return item
|
||||||
if self._data['page_info']['has_next_page']:
|
if self._data['page_info']['has_next_page']:
|
||||||
query_response = self._query(self._data['page_info']['end_cursor'])
|
query_response = self._query(self._data['page_info']['end_cursor'])
|
||||||
page_index, data = self._page_index, self._data
|
if self._data['edges'] != query_response['edges']:
|
||||||
try:
|
page_index, data = self._page_index, self._data
|
||||||
self._page_index = 0
|
try:
|
||||||
self._data = query_response
|
self._page_index = 0
|
||||||
except KeyboardInterrupt:
|
self._data = query_response
|
||||||
self._page_index, self._data = page_index, data
|
except KeyboardInterrupt:
|
||||||
raise
|
self._page_index, self._data = page_index, data
|
||||||
return self.__next__()
|
raise
|
||||||
|
return self.__next__()
|
||||||
raise StopIteration()
|
raise StopIteration()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
+33
-25
@@ -3,7 +3,7 @@ import lzma
|
|||||||
import re
|
import re
|
||||||
from base64 import b64decode, b64encode
|
from base64 import b64decode, b64encode
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union
|
from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union
|
||||||
|
|
||||||
@@ -35,8 +35,8 @@ PostLocation.id.__doc__ = "ID number of location."
|
|||||||
PostLocation.name.__doc__ = "Location name."
|
PostLocation.name.__doc__ = "Location name."
|
||||||
PostLocation.slug.__doc__ = "URL friendly variant of location name."
|
PostLocation.slug.__doc__ = "URL friendly variant of location name."
|
||||||
PostLocation.has_public_page.__doc__ = "Whether location has a public page."
|
PostLocation.has_public_page.__doc__ = "Whether location has a public page."
|
||||||
PostLocation.lat.__doc__ = "Latitude (:class:`float`)."
|
PostLocation.lat.__doc__ = "Latitude (:class:`float` or None)."
|
||||||
PostLocation.lng.__doc__ = "Longitude (:class:`float`)."
|
PostLocation.lng.__doc__ = "Longitude (:class:`float` or None)."
|
||||||
|
|
||||||
|
|
||||||
class Post:
|
class Post:
|
||||||
@@ -225,16 +225,12 @@ class Post:
|
|||||||
@property
|
@property
|
||||||
def date_local(self) -> datetime:
|
def date_local(self) -> datetime:
|
||||||
"""Timestamp when the post was created (local time zone)."""
|
"""Timestamp when the post was created (local time zone)."""
|
||||||
return datetime.fromtimestamp(self._node["date"]
|
return datetime.fromtimestamp(self.get_timestamp_date_created()).astimezone()
|
||||||
if "date" in self._node
|
|
||||||
else self._node["taken_at_timestamp"])
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date_utc(self) -> datetime:
|
def date_utc(self) -> datetime:
|
||||||
"""Timestamp when the post was created (UTC)."""
|
"""Timestamp when the post was created (UTC)."""
|
||||||
return datetime.utcfromtimestamp(self._node["date"]
|
return datetime.utcfromtimestamp(self.get_timestamp_date_created()).replace(tzinfo=timezone.utc)
|
||||||
if "date" in self._node
|
|
||||||
else self._node["taken_at_timestamp"])
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date(self) -> datetime:
|
def date(self) -> datetime:
|
||||||
@@ -275,6 +271,12 @@ class Post:
|
|||||||
return len(edges)
|
return len(edges)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
def get_timestamp_date_created(self) -> float:
|
||||||
|
"""Timestamp when the post was created"""
|
||||||
|
return (self._node["date"]
|
||||||
|
if "date" in self._node
|
||||||
|
else self._node["taken_at_timestamp"])
|
||||||
|
|
||||||
def get_is_videos(self) -> List[bool]:
|
def get_is_videos(self) -> List[bool]:
|
||||||
"""
|
"""
|
||||||
Return a list containing the ``is_video`` property for each media in the post.
|
Return a list containing the ``is_video`` property for each media in the post.
|
||||||
@@ -377,20 +379,23 @@ class Post:
|
|||||||
def video_url(self) -> Optional[str]:
|
def video_url(self) -> Optional[str]:
|
||||||
"""URL of the video, or None."""
|
"""URL of the video, or None."""
|
||||||
if self.is_video:
|
if self.is_video:
|
||||||
version_urls = [self._field('video_url')]
|
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:
|
if self._context.iphone_support and self._context.is_logged_in:
|
||||||
try:
|
try:
|
||||||
version_urls.extend(version['url'] for version in self._iphone_struct['video_versions'])
|
version_urls.extend(version['url'] for version in self._iphone_struct['video_versions'])
|
||||||
except (InstaloaderException, KeyError, IndexError) as err:
|
except (InstaloaderException, KeyError, IndexError) as err:
|
||||||
self._context.error(f"Unable to fetch high-quality video version of {self}: {err}")
|
self._context.error(f"Unable to fetch high-quality video version of {self}: {err}")
|
||||||
return version_urls[0]
|
version_urls = list(dict.fromkeys(version_urls))
|
||||||
else:
|
if len(version_urls) == 0:
|
||||||
|
return None
|
||||||
|
if len(version_urls) == 1:
|
||||||
return version_urls[0]
|
return version_urls[0]
|
||||||
url_candidates: List[Tuple[int, str]] = []
|
url_candidates: List[Tuple[int, str]] = []
|
||||||
for idx, version_url in enumerate(version_urls):
|
for idx, version_url in enumerate(version_urls):
|
||||||
if any(url_candidate[1] == version_url for url_candidate in url_candidates):
|
|
||||||
# Skip duplicates
|
|
||||||
continue
|
|
||||||
try:
|
try:
|
||||||
url_candidates.append((
|
url_candidates.append((
|
||||||
int(self._context.head(version_url, allow_redirects=True).headers.get('Content-Length', 0)),
|
int(self._context.head(version_url, allow_redirects=True).headers.get('Content-Length', 0)),
|
||||||
@@ -578,7 +583,7 @@ class Post:
|
|||||||
loc.update(self._context.get_json("explore/locations/{0}/".format(location_id),
|
loc.update(self._context.get_json("explore/locations/{0}/".format(location_id),
|
||||||
params={'__a': 1})['native_location_data']['location_info'])
|
params={'__a': 1})['native_location_data']['location_info'])
|
||||||
self._location = PostLocation(location_id, loc['name'], loc['slug'], loc['has_public_page'],
|
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
|
return self._location
|
||||||
|
|
||||||
|
|
||||||
@@ -1076,12 +1081,12 @@ class StoryItem:
|
|||||||
@property
|
@property
|
||||||
def date_local(self) -> datetime:
|
def date_local(self) -> datetime:
|
||||||
"""Timestamp when the StoryItem was created (local time zone)."""
|
"""Timestamp when the StoryItem was created (local time zone)."""
|
||||||
return datetime.fromtimestamp(self._node['taken_at_timestamp'])
|
return datetime.fromtimestamp(self._node['taken_at_timestamp']).astimezone()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date_utc(self) -> datetime:
|
def date_utc(self) -> datetime:
|
||||||
"""Timestamp when the StoryItem was created (UTC)."""
|
"""Timestamp when the StoryItem was created (UTC)."""
|
||||||
return datetime.utcfromtimestamp(self._node['taken_at_timestamp'])
|
return datetime.utcfromtimestamp(self._node['taken_at_timestamp']).replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date(self) -> datetime:
|
def date(self) -> datetime:
|
||||||
@@ -1129,20 +1134,23 @@ class StoryItem:
|
|||||||
def video_url(self) -> Optional[str]:
|
def video_url(self) -> Optional[str]:
|
||||||
"""URL of the video, or None."""
|
"""URL of the video, or None."""
|
||||||
if self.is_video:
|
if self.is_video:
|
||||||
version_urls = [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:
|
if self._context.iphone_support and self._context.is_logged_in:
|
||||||
try:
|
try:
|
||||||
version_urls.extend(version['url'] for version in self._iphone_struct['video_versions'])
|
version_urls.extend(version['url'] for version in self._iphone_struct['video_versions'])
|
||||||
except (InstaloaderException, KeyError, IndexError) as err:
|
except (InstaloaderException, KeyError, IndexError) as err:
|
||||||
self._context.error(f"Unable to fetch high-quality video version of {self}: {err}")
|
self._context.error(f"Unable to fetch high-quality video version of {self}: {err}")
|
||||||
return version_urls[0]
|
version_urls = list(dict.fromkeys(version_urls))
|
||||||
else:
|
if len(version_urls) == 0:
|
||||||
|
return None
|
||||||
|
if len(version_urls) == 1:
|
||||||
return version_urls[0]
|
return version_urls[0]
|
||||||
url_candidates: List[Tuple[int, str]] = []
|
url_candidates: List[Tuple[int, str]] = []
|
||||||
for idx, version_url in enumerate(version_urls):
|
for idx, version_url in enumerate(version_urls):
|
||||||
if any(url_candidate[1] == version_url for url_candidate in url_candidates):
|
|
||||||
# Skip duplicates
|
|
||||||
continue
|
|
||||||
try:
|
try:
|
||||||
url_candidates.append((
|
url_candidates.append((
|
||||||
int(self._context.head(version_url, allow_redirects=True).headers.get('Content-Length', 0)),
|
int(self._context.head(version_url, allow_redirects=True).headers.get('Content-Length', 0)),
|
||||||
@@ -1553,7 +1561,7 @@ class TopSearchResults:
|
|||||||
place = location.get('place', {})
|
place = location.get('place', {})
|
||||||
slug = place.get('slug')
|
slug = place.get('slug')
|
||||||
loc = place.get('location', {})
|
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]:
|
def get_hashtag_strings(self) -> Iterator[str]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user