Compare commits

..
11 Commits
Author SHA1 Message Date
Alexander Graf 47db7f7ffa Release of version 4.0.5 2018-07-09 10:18:27 +02:00
Alexander Graf 2567bec975 unittests: Test order of Profile.get_posts()
Motivated by #140.
2018-07-09 10:08:18 +02:00
Alexander Graf 2df1419d68 Fix file extension of profile picture
Fix #142.
2018-07-09 10:07:16 +02:00
Alexander Graf 8211c9daf1 Documentation Improvements
- Fix Spelling
- codesnippets: Improve introduction texts; Write subsection about
  metadata JSON files
2018-06-17 18:10:19 +02:00
Alexander Graf 969d4e3aa4 Post: Write all ever-obtained metadata into JSON
The Post._asdict() code assumed that Post._node always is a subset of
Post._full_metadata_dict. However, it turned out, that `edge_liked_by`,
`thumbnail_src` and `thumbnail_resources` may be contained in Post._node, but
not in Post._full_metadata_dict. Now the keys of both dictionaries are saved in
the JSON file.

Fixes #137.
2018-06-15 17:07:28 +02:00
Alexander Graf 4cf1ae847d Mention donation option in doc
(Suggested by #130)
2018-06-13 08:39:29 +02:00
André Koch-Kramer a276a0dc59 Release of version 4.0.4 2018-06-07 15:46:47 +02:00
André Koch-Kramer cc34b8340f Fix detection of sidecars and videos
This was noticeable when downloading hashtags.
Fixes #133
2018-06-07 15:32:23 +02:00
Alexander Graf 45fe509ac4 doc: Link codesnippets in basic-usage 2018-06-04 12:06:41 +02:00
Alexander Graf ae492ed68b New doc section: codesnippets / Advanced Examples
Presents code examples that use the instaloader module for more advanced tasks
than what is possible with the Instaloader command line interface.

Presents #46, #56, #110, #113, #120, #121.
2018-06-04 11:57:28 +02:00
Alexander Graf 0e433294ad unittests: Preserve query timestamps between tests
This avoids getting rate limited while running all the tests.
2018-05-30 13:16:16 +02:00
16 changed files with 316 additions and 26 deletions
+2
View File
@@ -50,6 +50,8 @@
<li><a href="https://github.com/instaloader/instaloader">Instaloader on GitHub</a></li>
<li role="separator" class="divider"></li>
<li style="padding-left: 20px;"><a class="github-button" href="https://github.com/instaloader/instaloader" data-icon="octicon-star" data-size="large" data-show-count="true" aria-label="Star Instaloader on GitHub">Star</a></li>
<li role="separator" class="divider"></li>
<li><a href="https://www.paypal.me/aandergr"><b>Donate (via Paypal)</b></a></li>
</ul>
</li>
</ul>
+3
View File
@@ -97,6 +97,9 @@ metadata of a Profile. :class:`Profile` instances can be created with:
A reference of the many methods provided by the :mod:`instaloader` module is
provided in the remainder of this document.
For a list of real code examples using the Instaloader module for advanced
tasks, see :ref:`codesnippets`.
``Instaloader`` (Main Class)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+5 -2
View File
@@ -68,7 +68,7 @@ Instaloader supports the following targets:
**download the user's stories**.
- ``"#hashtag"``
Posts with a certain **hashtag** (the quotes are usually neccessary),
Posts with a certain **hashtag** (the quotes are usually necessary),
- ``:stories``
The currently-visible **stories** of your followees (requires
@@ -158,7 +158,7 @@ Filter Posts
.. py:currentmodule:: instaloader
The options :option:`--post-filter` and :option:`--storyitem-filter`
allows to specify criterias that posts or story items have to
allows to specify criteria that posts or story items have to
meet to be downloaded. If not given, all posts are downloaded.
The filter string must be a
@@ -273,3 +273,6 @@ consider taking a look at the :ref:`python-module-instaloader`.
Instaloader exposes its internally used methods and structures, making it a
powerful and intuitive Python API for Instagram, allowing to further customize
obtaining media and metadata.
Also see :ref:`codesnippets`, where we collect example scripts that use
Instaloader to achieve more complex tasks.
+128
View File
@@ -0,0 +1,128 @@
.. _codesnippets:
Advanced Instaloader Examples
=============================
.. currentmodule:: instaloader
.. highlight:: python
.. contents::
:backlinks: none
Here we present code examples that use the :ref:`python-module-instaloader` for
more advanced Instagram downloading or metadata mining than what is possible
with the Instaloader command line interface.
.. For each code snippet:
- title
- brief description of what it does / motivation / how it works
- code, or link to code
- link to discussion issue
- link used methods
Download Posts in a Specific Period
-----------------------------------
To only download Instagram pictures (and metadata) that are within a specific
period, you can play around with :func:`~itertools.dropwhile` and
:func:`~itertools.takewhile` from :mod:`itertools` like in this snippet.
.. literalinclude:: codesnippets/121_since_until.py
See also :class:`Post`, :meth:`Instaloader.download_post`.
Discussed in :issue:`121`.
Likes of a Profile / Ghost Followers
------------------------------------
To obtain a list of your inactive followers, i.e. followers that did not like
any of your pictures, into a file you can use this approach.
.. literalinclude:: codesnippets/120_ghost_followers.py
See also :meth:`Profile.get_posts`, :meth:`Post.get_likes`,
:meth:`Profile.get_followers`, :meth:`Instaloader.load_session_from_file`,
:meth:`Profile.from_username`.
Discussed in :issue:`120`.
Track Deleted Posts
-------------------
This script uses Instaloader to obtain a list of currently-online Instagram and
compares it with the set of posts that you already have downloaded. It outputs
a list of posts which are online but not offline (i.e. not yet downloaded) and a
list of posts which are offline but not online (i.e. deleted in the profile).
.. literalinclude:: codesnippets/56_track_deleted.py
See also :func:`load_structure_from_file`, :meth:`Profile.from_username`,
:meth:`Profile.get_posts`, :class:`Post`.
Discussed in :issue:`56`.
Only one Post per User
----------------------
To download only the one most recent post from each user, this snippet creates a
:class:`set` that contains the users of which a post has already been
downloaded. While iterating the posts, it checks whether the post's owner
already is in the set. If not, the post is downloaded from Instagram and the
user is added to that set.
.. literalinclude:: codesnippets/113_only_one_per_user.py
See also :class:`Post`, :meth:`Instaloader.download_post`,
:attr:`Post.owner_profile`, :class:`Profile`.
Discussed in :issue:`113`.
Upgrade Images by Local Copies
------------------------------
The following script finds local versions of images fetched by Instaloader, in
order to upgrade the downloaded images by locally-found versions with better
quality. It uses image hashing to identify similar images.
`updgrade-instaloader-images.py <https://gist.github.com/pavelkryukov/15f93d19a99428a284a8bcec27e0187b>`__ (external link to GitHub Gist)
Discussed in :issue:`46`.
Add Captions to Images
----------------------
Instaloader does not modify the downloaded JPEG file. However, one could combine
it with an imaging library such as Pillow or PIL to render the caption on
Instagram pictures. The following shows an approach.
.. literalinclude:: codesnippets/110_pil_captions.py
See also :attr:`Post.caption`, :attr:`Post.url`, :meth:`Post.from_shortcode`,
:func:`load_structure_from_file`.
Discussed in :issue:`110`.
Metadata JSON Files
-------------------
The JSON files Instaloader saves along with each Post contain all the metadata
that has been retrieved from Instagram while downloading the picture and
associated required information.
With `jq <https://stedolan.github.io/jq/>`__, a command-line JSON processor, the
metadata can be easily post-processed. For example, Instaloader's JSON files can
be pretty-formatted with:
.. code-block:: none
xzcat 2018-05-13_11-18-45_UTC.json.xz | jq .node
However, Instaloader tries to do as few metadata requests as possible, so,
depending on how Instaloader has been invoked, it may occur that these files do
not contain the complete available metadata structure. Nevertheless, the file
can be loaded into Instaloader with :func:`load_structure_from_file` and the
required metadata then be accessed via the :class:`Post` or :class:`Profile`
attributes, which trigger an Instagram request if that particular information is
not present in the JSON file.
+20
View File
@@ -0,0 +1,20 @@
from io import BytesIO
from requests import get
from PIL import Image, ImageDraw
from instaloader import *
L = Instaloader()
# Load Post instance
post = load_structure_from_file(L.context, '2017-10-01_18-53-03_UTC.json.xz')
# or post = Post.from_shortcode(L.context, SHORTCODE)
# Render caption
image = Image.open(BytesIO(get(post.url).content))
draw = ImageDraw.Draw(image)
color = 'rgb(0, 0, 0)' # black color
draw.text((300,100), post.caption.encode('latin1', errors='ignore'), fill=color)
# Save image
image.save('test.jpg')
@@ -0,0 +1,14 @@
import instaloader
L = instaloader.Instaloader()
posts = L.get_hashtag_posts('milfgarden')
users = set()
for post in posts:
if not post.owner_profile in users:
L.download_post(post, '#milfgarden')
users.add(post.owner_profile)
else:
print("{} from {} skipped.".format(post, post.owner_profile))
+27
View File
@@ -0,0 +1,27 @@
import instaloader
L = instaloader.Instaloader()
USER = 'your_account'
PROFILE = USER
# Your preferred way of logging in:
L.load_session_from_file(USER)
profile = instaloader.Profile.from_username(L.context, PROFILE)
likes = set()
print('Fetching likes of all posts of profile {}.'.format(profile.username))
for post in profile.get_posts():
print(post)
likes = likes | set(post.get_likes())
print('Fetching followers of profile {}.'.format(profile.username))
followers = set(profile.get_followers())
ghosts = followers - likes
print('Storing ghosts into file.')
with open('/YOUR PATH/inactive-users.txt', 'w') as f:
for ghost in ghosts:
print(ghost.username, file=f)
+17
View File
@@ -0,0 +1,17 @@
from datetime import datetime
from itertools import dropwhile, takewhile
import instaloader
L = instaloader.Instaloader()
posts = L.get_hashtag_posts('milfgarden')
# or
# posts = instaloader.Profile.from_username(L.context, PROFILE).get_posts()
SINCE = datetime(2015, 5, 1)
UNTIL = datetime(2015, 3, 1)
for post in takewhile(lambda p: p.date > UNTIL, dropwhile(lambda p: p.date > SINCE, posts)):
print(post.date)
L.download_post(post, '#milfgarden')
+34
View File
@@ -0,0 +1,34 @@
from glob import glob
from sys import argv
from os import chdir
from instaloader import Instaloader, Post, Profile, load_structure_from_file
# Instaloader instantiation - you may pass additional arguments to the constructor here
L = Instaloader()
# If desired, load session previously saved with `instaloader -l USERNAME`:
#L.load_session_from_file(USERNAME)
try:
TARGET = argv[1]
except IndexError:
raise SystemExit("Pass profile name as argument!")
# Obtain set of posts that are on hard disk
chdir(TARGET)
offline_posts = set(filter(lambda s: isinstance(s, Post),
(load_structure_from_file(L.context, file)
for file in (glob('*.json.xz') + glob('*.json')))))
# Obtain set of posts that are currently online
post_iterator = Profile.from_username(L.context, TARGET).get_posts()
online_posts = set(post_iterator)
if online_posts - offline_posts:
print("Not yet downloaded posts:")
print(" ".join(str(p) for p in (online_posts - offline_posts)))
if offline_posts - online_posts:
print("Deleted posts:")
print(" ".join(str(p) for p in (offline_posts - online_posts)))
+4 -1
View File
@@ -39,7 +39,8 @@ import docs.sphinx_autodoc_typehints as sphinx_autodoc_typehints
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.githubpages',
'sphinx.ext.intersphinx'
'sphinx.ext.intersphinx',
'sphinx.ext.extlinks'
]
autodoc_default_flags = ['show-inheritance', 'members', 'undoc-members']
@@ -49,6 +50,8 @@ autodoc_member_order = 'bysource'
intersphinx_mapping = {'python': ('https://docs.python.org/3', None),
'requests': ('http://docs.python-requests.org/en/master', None)}
extlinks = {'issue': ('https://github.com/instaloader/instaloader/issues/%s', 'issue #')}
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
+25 -5
View File
@@ -1,3 +1,5 @@
.. _contributing:
Contributing to Instaloader
===========================
@@ -23,10 +25,10 @@ following in mind:
- Briefly **check whether the bug has already been reported**. If you find an
issue reporting the same bug you encountered, comment there rather than
opening a new issue. However, if unsure, it is advised to create a new issue.
opening a new issue. However, if unsure, please create a new issue.
- State **how the bug can be reproduced**, i.e. how Instaloader was invoked
when the problem occured (of course, you may anonymize profile names etc.).
when the problem occurred (of course, you may anonymize profile names etc.).
- Include all **error messages and tracebacks** in the report.
@@ -47,13 +49,13 @@ few things to consider:
you forked from the most recent branch.
- We use `Pylint <https://www.pylint.org/>`__ for error and syntax checking of
``instaloader.py``. The file ``.travis.yml`` in the project's root directory
the source. The file ``.travis.yml`` in the project's root directory
shows how it is invoked. Note that sometimes it might be better to disable a
warning rather than adapting the code to Pylint's desires.
- The documentation source is located in the ``docs`` folder. The file
``cli-options.rst`` is merely an RST-formatted copy of ``ìnstaloader --help``
output, of which the source is near the bottom of ``instaloader.py``.
``cli-options.rst`` is merely an RST-formatted copy of ``instaloader --help``
output, of which the source is in ``instaloader/__main__.py``.
- Feel free to contact us, even if you "only" have Proof-of-Concepts or
not-fully integrated changes. They already might be an advance for the
@@ -81,3 +83,21 @@ Please consider the following:
- **Be patient**. Naturally, bugs and pull requests have a higher priority than
feature suggestions. Keep in mind that this is a free software project, and
unfortunately we only have limited time to work on it.
Donations
---------
.. 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.
We have a PayPal.Me link for donations: `PayPal.me/aandergr
<https://www.paypal.me/aandergr>`__
.. donations-end
.. (Discussion in :issue:`130`)
+14
View File
@@ -59,6 +59,7 @@ Instaloader Documentation
basic-usage
cli-options
as-module
codesnippets
contributing
Useful Links
@@ -69,10 +70,23 @@ 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
------------
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
:start-after: donations-start
:end-before: donations-end
..
* :ref:`genindex`
+1 -1
View File
@@ -1,7 +1,7 @@
"""Download pictures (or videos) along with their captions and other metadata from Instagram."""
__version__ = '4.0.3'
__version__ = '4.0.5'
try:
+3 -2
View File
@@ -270,14 +270,15 @@ class Instaloader:
with self.context.get_anonymous_session() as anonymous_session:
date_object = datetime.strptime(anonymous_session.head(profile_pic_url).headers["Last-Modified"],
'%a, %d %b %Y %H:%M:%S GMT')
profile_pic_extension = 'jpg'
if ((format_string_contains_key(self.dirname_pattern, 'profile') or
format_string_contains_key(self.dirname_pattern, 'target'))):
filename = '{0}/{1}_UTC_profile_pic.{2}'.format(self.dirname_pattern.format(profile=profile.username.lower(),
target=profile.username.lower()),
_epoch_to_string(date_object), profile_pic_url[-3:])
_epoch_to_string(date_object), profile_pic_extension)
else:
filename = '{0}/{1}_{2}_UTC_profile_pic.{3}'.format(self.dirname_pattern.format(), profile.username.lower(),
_epoch_to_string(date_object), profile_pic_url[-3:])
_epoch_to_string(date_object), profile_pic_extension)
if os.path.isfile(filename):
self.context.log(filename + ' already exists')
return None
+3 -7
View File
@@ -93,10 +93,9 @@ class Post:
return b64encode(mediaid.to_bytes(9, 'big'), b'-_').decode().replace('A', ' ').lstrip().replace(' ', 'A')
def _asdict(self):
node = self._node
if self._full_metadata_dict:
node = self._full_metadata_dict
else:
node = self._node
node.update(self._full_metadata_dict)
if self._owner_profile:
node['owner'] = self.owner_profile._asdict()
if self._location:
@@ -206,10 +205,7 @@ class Post:
@property
def typename(self) -> str:
"""Type of post, GraphImage, GraphVideo or GraphSidecar"""
if '__typename' in self._node:
return self._node['__typename']
# if __typename is not in node, it is an old image or video
return 'GraphImage'
return self._field('__typename')
def get_sidecar_nodes(self) -> Iterator[PostSidecarNode]:
"""Sidecar nodes of a Post with typename==GraphSidecar."""
+16 -8
View File
@@ -16,6 +16,9 @@ NORMAL_MAX_COUNT = 2
PAGING_MAX_COUNT = 15
PRIVATE_PROFILE = "aandergr"
# Preserve query timestamps (rate control) between tests to not get rate limited
instaloadercontext_query_timestamps = list()
class TestInstaloaderAnonymously(unittest.TestCase):
@@ -27,23 +30,31 @@ class TestInstaloaderAnonymously(unittest.TestCase):
download_comments=True,
save_metadata=True)
self.L.context.raise_all_errors = True
self.L.context.query_timestamps = instaloadercontext_query_timestamps
def tearDown(self):
global instaloadercontext_query_timestamps
instaloadercontext_query_timestamps = self.L.context.query_timestamps
self.L.close()
os.chdir('/')
print("Removing {}".format(self.dir))
shutil.rmtree(self.dir)
def post_paging_test(self, iterator):
previous_post = None
for post in islice(iterator, PAGING_MAX_COUNT):
print(post)
if previous_post:
self.assertTrue(post.date_utc < previous_post.date_utc)
previous_post = post
@unittest.SkipTest
def test_public_profile_download(self):
self.L.download_profile(PUBLIC_PROFILE, profile_pic=False, fast_update=True)
self.L.download_profile(PUBLIC_PROFILE, profile_pic=False, fast_update=True)
def test_public_profile_paging(self):
for count, post in enumerate(instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).get_posts()):
print(post)
if count == PAGING_MAX_COUNT:
break
self.post_paging_test(instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).get_posts())
def test_profile_pic_download(self):
self.L.download_profile(PUBLIC_PROFILE, profile_pic_only=True)
@@ -87,10 +98,7 @@ class TestInstaloaderLoggedIn(TestInstaloaderAnonymously):
print(item)
def test_private_profile_paging(self):
for count, post in enumerate(instaloader.Profile.from_username(self.L.context, PRIVATE_PROFILE).get_posts()):
print(post)
if count == PAGING_MAX_COUNT:
break
self.post_paging_test(instaloader.Profile.from_username(self.L.context, PRIVATE_PROFILE).get_posts())
def test_feed_download(self):
self.L.download_feed_posts(NORMAL_MAX_COUNT)