1
0
mirror of https://github.com/instaloader/instaloader.git synced 2024-08-17 20:29:38 +02:00

Add class and functions for downloading highlights

Requested in #162.
This commit is contained in:
André Koch-Kramer 2018-08-11 07:25:33 +02:00
parent 0dcc912987
commit 91d5d5f867
4 changed files with 115 additions and 5 deletions

View File

@ -135,6 +135,11 @@ User Stories
.. autoclass:: StoryItem .. autoclass:: StoryItem
:no-show-inheritance: :no-show-inheritance:
Highlights
""""""""""
.. autoclass:: Highlight
Profiles Profiles
"""""""" """"""""

View File

@ -15,5 +15,5 @@ else:
from .exceptions import * from .exceptions import *
from .instaloader import Instaloader from .instaloader import Instaloader
from .instaloadercontext import InstaloaderContext from .instaloadercontext import InstaloaderContext
from .structures import (Post, PostSidecarNode, PostComment, PostLocation, Profile, Story, StoryItem, from .structures import (Highlight, Post, PostSidecarNode, PostComment, PostLocation, Profile, Story, StoryItem,
load_structure_from_file, save_structure_to_file) load_structure_from_file, save_structure_to_file)

View File

@ -15,7 +15,7 @@ from typing import Any, Callable, Iterator, List, Optional, Set, Union
from .exceptions import * from .exceptions import *
from .instaloadercontext import InstaloaderContext from .instaloadercontext import InstaloaderContext
from .structures import JsonExportable, Post, PostLocation, Profile, Story, StoryItem, save_structure_to_file from .structures import Highlight, JsonExportable, Post, PostLocation, Profile, Story, StoryItem, save_structure_to_file
def get_default_session_filename(username: str) -> str: def get_default_session_filename(username: str) -> str:
@ -190,7 +190,7 @@ class Instaloader:
def update_comments(self, filename: str, post: Post) -> None: def update_comments(self, filename: str, post: Post) -> None:
def _postcomment_asdict(comment): def _postcomment_asdict(comment):
return {'id': comment.id, return {'id': comment.unique_id,
'created_at': int(comment.created_at_utc.replace(tzinfo=timezone.utc).timestamp()), 'created_at': int(comment.created_at_utc.replace(tzinfo=timezone.utc).timestamp()),
'text': comment.text, 'text': comment.text,
'owner': comment.owner._asdict()} 'owner': comment.owner._asdict()}
@ -488,6 +488,56 @@ class Instaloader:
self.context.log() self.context.log()
return downloaded return downloaded
@_requires_login
def get_highlights(self, userid: int) -> Iterator[Highlight]:
"""Get all highlights from a user.
To use this, one needs to be logged in
:param userid: ID of the profile whose highlights should get fetched.
"""
data = self.context.graphql_query("7c16654f22c819fb63d1183034a5162f",
{"user_id": userid, "include_chaining": False, "include_reel": False,
"include_suggested_users": False, "include_logged_out_extras": False,
"include_highlight_reels": True})["data"]["user"]['edge_highlight_reels']
if data is None:
raise BadResponseException('Bad highlights reel JSON.')
yield from (Highlight(self.context, edge['node']) for edge in data['edges'])
@_requires_login
def download_highlights(self,
userid: int,
fast_update: bool = False,
filename_target: Optional[str] = None,
storyitem_filter: Optional[Callable[[StoryItem], bool]] = None) -> None:
"""
Download available highlights from a user whose ID is given.
To use this, one needs to be logged in
:param userid: ID of the profile whose highlights should get downloaded.
:param fast_update: If true, abort when first already-downloaded picture is encountered
:param filename_target: Replacement for {target} in dirname_pattern and filename_pattern
or None if profile name and the highlights' titles should be used instead
:param storyitem_filter: function(storyitem), which returns True if given StoryItem should be downloaded
"""
for user_highlight in self.get_highlights(userid):
name = user_highlight.owner_username
self.context.log("Retrieving highlights \"{}\" from profile {}".format(user_highlight.title, name))
totalcount = user_highlight.itemcount
count = 1
for item in user_highlight.get_items():
if storyitem_filter is not None and not storyitem_filter(item):
self.context.log("<{} skipped>".format(item), flush=True)
continue
self.context.log("[%3i/%3i] " % (count, totalcount), end="", flush=True)
count += 1
with self.context.error_catcher('Download highlights \"{}\" from user {}'.format(user_highlight.title, name)):
downloaded = self.download_storyitem(item, filename_target
if filename_target
else '{}/{}'.format(name, user_highlight.title))
if fast_update and not downloaded:
break
@_requires_login @_requires_login
def get_feed_posts(self) -> Iterator[Post]: def get_feed_posts(self) -> Iterator[Post]:
"""Get Posts of the user's feed. """Get Posts of the user's feed.

View File

@ -782,7 +782,7 @@ class Story:
# story is a Story object # story is a Story object
for item in story.get_items(): for item in story.get_items():
# item is a StoryItem object # item is a StoryItem object
L.download_storyitem(item, ':stores') L.download_storyitem(item, ':stories')
This class implements == and is hashable. This class implements == and is hashable.
@ -805,7 +805,7 @@ class Story:
return NotImplemented return NotImplemented
def __hash__(self) -> int: def __hash__(self) -> int:
return hash(self._unique_id) return hash(self.unique_id)
@property @property
def unique_id(self) -> str: def unique_id(self) -> str:
@ -868,6 +868,61 @@ class Story:
yield from (StoryItem(self._context, item, self.owner_profile) for item in reversed(self._node['items'])) yield from (StoryItem(self._context, item, self.owner_profile) for item in reversed(self._node['items']))
class Highlight(Story):
def __init__(self, context: InstaloaderContext, node: Dict[str, Any]):
super().__init__(context, node)
self._items = None
def __repr__(self):
return '<Highlight by {}: {}>'.format(self.owner_username, self.title)
@property
def unique_id(self) -> int:
"""A unique ID identifying this set of highlights."""
return int(self._node['id'])
@property
def owner_profile(self) -> Profile:
""":class:`Profile` instance of the highlights' owner."""
if not self._owner_profile:
self._owner_profile = Profile(self._context, self._node['owner'])
return self._owner_profile
@property
def title(self) -> str:
"""The title of these highlights."""
return self._node['title']
@property
def cover_url(self) -> str:
"""URL of the highlights' cover."""
return self._node['cover_media']['thumbnail_src']
@property
def cover_cropped_url(self) -> str:
"""URL of the cropped version of the cover."""
return self._node['cover_media_cropped_thumbnail']['url']
def _fetch_items(self):
if not self._items:
self._items = self._context.graphql_query("45246d3fe16ccc6577e0bd297a5db1ab",
{"reel_ids": [], "tag_names": [], "location_ids": [],
"highlight_reel_ids": [str(self.unique_id)],
"precomposed_overlay": False})['data']['reels_media'][0]['items']
@property
def itemcount(self) -> int:
"""Count of items associated with the :class:`Highlight` instance."""
self._fetch_items()
return len(self._items)
def get_items(self) -> Iterator[StoryItem]:
"""Retrieve all associated highlight items."""
self._fetch_items()
yield from (StoryItem(self._context, item, self.owner_profile) for item in self._items)
JsonExportable = Union[Post, Profile, StoryItem] JsonExportable = Union[Post, Profile, StoryItem]