Compare commits

...
10 Commits
Author SHA1 Message Date
Alexander Graf d6e5e31005 Release of Version 4.8.3 2022-01-07 14:56:40 +01:00
Alexander Graf d864ce08ff Make {Post,StoryItem}.date_utc timezone aware
Also simplify {Post,StoryItem}.date_local.

Discussed in #1316.
2022-01-07 14:53:23 +01:00
Eduardo Kalinowski 555c86633c Remove unnecessary conversion to timezone aware timestamp (#1372)
Post.date_local now already returns a timestamp with the appropriate
timezone.
2022-01-07 14:31:41 +01:00
Misael 21365ec668 Timezone option for datetime style formatting (#1316)
Add timezone awareness for datetime objects returned by `date_local` and `date_utc`.
Fixes #1305.
2021-12-31 16:50:16 +01:00
Alexander Graf 6343e9a69d Release of Version 4.8.2 2021-11-27 16:25:12 +01:00
Alexander Graf 06574eb428 Fix KeyError on attempt to get incomplete location
Fixes #1349.
2021-11-27 16:25:12 +01:00
JeremyKj87 815ba83d77 avoid comments endless loop - check if query_response is always the same (#1347) 2021-11-27 15:47:32 +01:00
fireattack 470fc0d905 Fix KeyError for video_url, and better workflow (#1321)
Fixes #1320.
2021-11-12 15:21:33 +01:00
Alexander Graf 0300d8a4f1 Fix CI for Python 3.10 2021-11-12 14:32:15 +01:00
Alexander Graf f09bd51ed4 Update feature suggestion issue template 2021-09-25 10:32:36 +02:00
8 changed files with 69 additions and 50 deletions
+9 -2
View File
@@ -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.
-->
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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 -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.8.1' __version__ = '4.8.3'
try: try:
+13 -10
View File
@@ -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"):
+9 -8
View File
@@ -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
View File
@@ -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]:
""" """