Compare commits

...
63 Commits
Author SHA1 Message Date
André Koch-Kramer edd1fbe49c Release of version 3.3.4 2018-03-29 20:36:53 +02:00
André Koch-Kramer dae18a05c6 Use anonymous session for profile pic query 2018-03-29 20:33:20 +02:00
André Koch-Kramer c5b60b8a12 GraphQL for post locations 2018-03-29 20:31:39 +02:00
André Koch-Kramer 40a87761c9 Mention Windows standalone exe in documentation 2018-03-29 19:47:35 +02:00
André Koch-Kramer d9b5c77e3a Implement high quality profile pic hack
Use 'i.instagram.com' API to retrieve the best quality version of the
profile pic.
Related to #85.
2018-03-29 19:33:05 +02:00
Alexander Graf 5b6e8f1a31 Fix indent in USAGE_STRING message 2018-03-29 11:19:15 +02:00
Alexander Graf d533c60824 doc: Switch to mobile-friendlier bootstrap theme
It seems to be better supported than Alabaster and is easier to be
configured to look good on both a phone and a desktop.
2018-03-28 19:39:58 +02:00
Alexander Graf 539273b963 make doc more mobile-friendly 2018-03-24 21:47:35 +01:00
André Koch-Kramer 71331d37e4 Removed modifying of profilepic url
Since Instagram does not allow anymore to download pics without a valid
signature given in the URL, Instaloader has to be content with a
resolution for the profile pic that is provided in the JSON data.
Closes #85.
2018-03-24 19:54:07 +01:00
Alexander Graf 36110c8727 Disable search engine indexing on old doc hoster 2018-03-24 12:37:24 +01:00
Alexander Graf 4a944fbbdb doc: meta tag to redirect to new documentation URL 2018-03-24 11:32:53 +01:00
Alexander Graf 6764cd961f Tweak documentation description html meta tags 2018-03-22 16:07:19 +01:00
Alexander Graf a125b367e3 Release of version 3.3.3 2018-03-22 14:56:29 +01:00
Alexander Graf 07b30d6ebd Log message if Instaloader is called w/out target 2018-03-22 13:42:08 +01:00
Alexander Graf 3640e9c056 Replace : by \ua789 (꞉) in pathnames on Windows
Workaround for #84.
2018-03-22 11:25:20 +01:00
André Koch-Kramer 90a1b73d21 Fix format_string_contains_key()
AttributeError occured if no parsable field was found in the specified
pattern.
Reported in #84.
2018-03-21 22:25:40 +01:00
André Koch-Kramer a7f89e4327 Release of version 3.3.2 2018-03-18 18:10:18 +01:00
André Koch-Kramer af1ec22d41 Fix get_feed_posts() using graphql queries
Due to changes at Instagram's API, it seems like it is no longer
possible to get users' feeds without usage of graphql queries on some
accounts.
2018-03-18 17:56:33 +01:00
André Koch-Kramer f55334cd5b Fix KeyError on login attempt with wrong password 2018-03-18 17:41:03 +01:00
André Koch-Kramer b2f31e7782 Add awesome logo to README
Due to rst limitations, the heading "Instaloader" was added beside the
logo inside the png.
2018-03-14 23:08:04 +01:00
Alexander Graf 68d8f65cb9 Add logo in documentation sidebar 2018-03-14 22:14:24 +01:00
Alexander Graf e97e4ecdc8 Further enhance logo 2018-03-14 19:41:20 +01:00
Alexander Graf e77c998a94 Invent beautiful logo and use it as doc favicon 2018-03-14 19:09:24 +01:00
Alexander Graf 861d7b92f1 Update Year in LICENSE file 2018-03-14 11:42:23 +01:00
Alexander Graf bfc882306b Release of version 3.3.1 2018-03-14 11:37:43 +01:00
Alexander Graf 14e9cf435e Fix download of saved posts
(was broken with commit 83b31175df, due to
a slight modification of Instaloader.get_profile_metadata)
2018-03-14 08:41:48 +01:00
Alexander Graf 83b31175df Support new GraphQL profile metadata structures
Fixes downloading of profiles, reported in #81.
2018-03-14 08:18:54 +01:00
Alexander Graf 92114f9018 Add message on instaloader.rtfd.io that doc moved 2018-03-13 16:58:00 +01:00
Alexander Graf 58a80c349d Release of version 3.3 2018-03-13 16:34:05 +01:00
Alexander Graf 5f34fca9e7 Merge branch 'v3.3-dev' 2018-03-13 16:22:37 +01:00
Alexander Graf fd6389249c Describe return type of Post.get_{likes,comments} 2018-03-13 16:19:26 +01:00
Alexander Graf 374f13adbf Introduce Instaloader.GRAPHQL_PAGE_LENGTH 2018-03-13 16:10:38 +01:00
Alexander Graf 17e443a7e2 Post.get_likes() to iterate over likes of Post
Wished in issue #70.
2018-03-13 15:57:12 +01:00
André Koch-Kramer 4628f1cc8d Reimplemented test_login() via graphql query
Fixes # 76.
2018-03-13 15:37:35 +01:00
Alexander Graf e97411a502 Mention :saved in all places of documentation 2018-03-13 15:36:27 +01:00
Alexander Graf 304cde64fe Link to github pages doc hosting rather than rtd 2018-03-13 15:11:36 +01:00
Alexander Graf 3dbce922c3 travis: remove checks on python 3.7 and nightly
These failed because of Sphinx.
2018-03-13 14:11:55 +01:00
Alexander Graf 4e57ac77f0 docs: Obtain current tag via git instead of gh api 2018-03-13 14:03:49 +01:00
André Koch-Kramer de960b2b57 Mentione target :saved in README, docs and --help 2018-03-13 13:53:21 +01:00
Alexander Graf 9b60adeb45 Adjust github repo references
New repo URL: https://github.com/instaloader/instaloader
2018-03-13 13:29:44 +01:00
Alexander Graf 0333bb5e99 Travis to push doc to instaloader.github.io 2018-03-13 13:22:10 +01:00
André Koch-Kramer b1edaddb31 Support for new target :saved
If logged in, Instaloader is now able to download posts which are marked
as saved. This feature was suggested in #78.
2018-03-13 12:02:45 +01:00
André Koch-Kramer 01c2a2b1cb Release of version 3.2.2 2018-02-15 16:13:36 +01:00
André Koch-Kramer 541b29b2e9 Remove signatures from profile pic URLs
Fixes #75 as suggested by @e5150.
2018-02-15 16:10:04 +01:00
André Koch-Kramer 6f3afd2fa1 Removed redundant character escapes in RegExp 2018-02-15 15:01:17 +01:00
André Koch-Kramer 882d460a67 Merge branch 'bug_69_fix' 2018-02-15 14:56:42 +01:00
André Koch-Kramer 9b11ca18d1 Added backward compatibility for --fast-update
This is related to bug #69.
Filenames containing the old date encoding without UTC are now
recognized when using the `--fast-update` switch.
Also non-picture files using the old date encoding should now be
recognized even when not using the `--fast-update` switch, excluding
geotags and the post metadata JSON files. The files containing comments
get renamed to the new format when touched. Caption files are only
renamed, if they get updated. Stored picture files do not get renamed at
all.
2018-01-28 16:40:13 +01:00
Alexander Graf 39bd7c55f6 doc: link next section at the end of each page
This makes it easier to go through the documentation and it
pushes readthedocs's advertising further apart from the
content.
2018-01-13 19:15:32 +01:00
André Koch-Kramer 3654d6bf6d Use UTC in filenames per default
This adresses bug #69.
All fields in the filename pattern that should be replaced by a date
encoding string (excluding {post.date_local}) are now evaluated using
UTC while ensuring that the replacing string indicates the use of UTC.
2018-01-10 22:26:49 +01:00
Alexander Graf 1ca368fa45 Mention Arch+Gentoo pkgs in doc/install section 2018-01-07 22:08:51 +01:00
Alexander Graf 698ea51cb9 Add contribution guidelines to documentation 2018-01-07 17:26:35 +01:00
Alexander Graf e6be0179b7 Support {date_utc} filename_pattern
{date_utc} encodes the post creation date in UTC rather than the
current local timezone, as {date} does.

This was proposed in #69. Encoding the post creation date in
local time zone induces problems regarding --fast-update when the
time zone is changed.
2018-01-07 14:52:03 +01:00
Alexander Graf 97e1bd2bf0 Disable Pylint's new inconsistent-return-statements 2017-12-21 14:03:44 +01:00
Alexander Graf 5695bca9f3 Current version and release date in doc sidebar 2017-12-21 13:53:06 +01:00
Alexander Graf 75d9dda4ba --no-video-thumbnails option
Closes #61.
2017-12-14 19:16:39 +01:00
Alexander Graf 41a50ffa5a Refactor download_story() into a method 2017-12-14 16:17:46 +01:00
Alexander Graf 3daaa6fbbc --no-profile-pic to disable profile pic download
(Suggested in #61)
2017-12-14 15:55:53 +01:00
André Koch-Kramer 341030058b Release of version 3.2.1 2017-12-11 21:21:07 +01:00
André Koch-Kramer fd98e8e78f Reduced GraphQL query chunks to 200
Fixes #59.
2017-12-11 21:11:18 +01:00
Alexander Graf e24c6a2cad Add description meta tags to documentation 2017-12-03 15:20:51 +01:00
Alexander Graf 01e549275e doc: ownership verification tag 2017-11-17 10:06:53 +01:00
Alexander Graf 37780986fc doc: Page intro improvements
The introductionary sections of as-module and cli-options have been
rewritten. Further, Instaloader's short-description is now included in
the sidebar.
2017-11-14 11:18:30 +01:00
André Koch-Kramer 9f34e3303e Added basic usage to README and documentation 2017-11-10 18:55:52 +01:00
25 changed files with 843 additions and 271 deletions
+19 -10
View File
@@ -4,17 +4,26 @@ python:
- "3.5-dev"
- "3.6"
- "3.6-dev"
- "3.7-dev"
- "nightly"
install:
- pip install pylint requests
- pip install -r docs/requirements.txt
script:
- python3 -m pylint -r n -d bad-whitespace,missing-docstring,too-many-arguments,locally-disabled,line-too-long,too-many-public-methods,too-many-lines,too-many-instance-attributes,too-many-locals,too-many-branches,too-many-statements instaloader
- python3 -m pylint -r n -d bad-whitespace,missing-docstring,too-many-arguments,locally-disabled,line-too-long,too-many-public-methods,too-many-lines,too-many-instance-attributes,too-many-locals,too-many-branches,too-many-statements,inconsistent-return-statements instaloader
- make -C docs html
deploy:
provider: pypi
user: aandergr
on:
tags: true
python: 3.6
password:
secure: KBsATtXNnEzqr0X2ALdyx2zaYj2EUfK8YjP4jEOilBELpJ4evHYXVgRkn9CKXGsD7LKbWmR3gz2rr8NZ6U42pHw0fpQHizz91MpJSKbNY7RZZh4UVWMDh81izeglchWAF+m3Wdpxpo/GBDbrZIbRQFhPgB+kjSzJo3uiCS+yVXtGG+6csJHzd0WkJjsVHz+XSdxyOiLy/qFF3nmY6ZrVrgw6WbX2YLbx9je6DxK3YRrnBqgMgH7Ui2xWWbgJCk3KKpQTMiMKnTa9mdeMJAd26YMCz1gG/X7ck6VNly/6bs5+Zq7/bU0dSolhPBhNthyccYdS0IeGPdPF3YqatblJ4fLGTfHn8oJs3/T11yIW9pvOT3UftEynPOUX+en2Qnb/3WfgwUfVts8i8DAYgEFLGEzbF5XMGy4gwAD/bZ0JW0pmgAG2PZsdOokAt+ydlNj/uxq1GFW2doZbul6k4t03p1mik6itYscvuhKoEm8Yw6wr1P9aDvMR6FFpSu4gDt2XrKmRmWkSXxpNeCfZujTjyAnISxLVQGSY7BG5K0Z2HbDtqqNz9nj4WgNGzZmngiRofCevWR+vPIDUDW7NCX03csfcFS9RRllBO/BrL7NVUV+Qp9dfTQYp+8SqYLWgHtDoXdgyMREjJaAmbx2u/TYTY8bADZ5F3vGUpv2j5hP3NTU=
- provider: pypi
user: aandergr
on:
tags: true
python: 3.6
password:
secure: KBsATtXNnEzqr0X2ALdyx2zaYj2EUfK8YjP4jEOilBELpJ4evHYXVgRkn9CKXGsD7LKbWmR3gz2rr8NZ6U42pHw0fpQHizz91MpJSKbNY7RZZh4UVWMDh81izeglchWAF+m3Wdpxpo/GBDbrZIbRQFhPgB+kjSzJo3uiCS+yVXtGG+6csJHzd0WkJjsVHz+XSdxyOiLy/qFF3nmY6ZrVrgw6WbX2YLbx9je6DxK3YRrnBqgMgH7Ui2xWWbgJCk3KKpQTMiMKnTa9mdeMJAd26YMCz1gG/X7ck6VNly/6bs5+Zq7/bU0dSolhPBhNthyccYdS0IeGPdPF3YqatblJ4fLGTfHn8oJs3/T11yIW9pvOT3UftEynPOUX+en2Qnb/3WfgwUfVts8i8DAYgEFLGEzbF5XMGy4gwAD/bZ0JW0pmgAG2PZsdOokAt+ydlNj/uxq1GFW2doZbul6k4t03p1mik6itYscvuhKoEm8Yw6wr1P9aDvMR6FFpSu4gDt2XrKmRmWkSXxpNeCfZujTjyAnISxLVQGSY7BG5K0Z2HbDtqqNz9nj4WgNGzZmngiRofCevWR+vPIDUDW7NCX03csfcFS9RRllBO/BrL7NVUV+Qp9dfTQYp+8SqYLWgHtDoXdgyMREjJaAmbx2u/TYTY8bADZ5F3vGUpv2j5hP3NTU=
- provider: pages
skip-cleanup: true
github-token: $GITHUB_TOKEN
local-dir: docs/_build/html
repo: instaloader/instaloader.github.io
target-branch: master
on:
branch: master
python: 3.6
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2016 Alexander Graf and André Koch-Kramer.
Copyright (c) 2016-2018 Alexander Graf and André Koch-Kramer.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+11 -6
View File
@@ -1,5 +1,4 @@
Instaloader
===========
.. image:: https://raw.githubusercontent.com/instaloader/instaloader/master/docs/logo_heading.png
::
@@ -9,8 +8,8 @@ Instaloader
**Instaloader**
- downloads **public and private profiles, hashtags, user stories and
feeds**,
- downloads **public and private profiles, hashtags, user stories,
feeds and saved media**,
- downloads **comments, geotags and captions** of each post,
@@ -20,7 +19,13 @@ Instaloader
- allows **fine-grained customization** of filters and where to store
downloaded media.
`Instaloader Documentation <https://instaloader.readthedocs.io/>`__
::
instaloader [--comments] [--geotags] [--stories]
[--login YOUR-USERNAME] [--fast-update]
profile | "#hashtag" | :stories | :feed | :saved
`Instaloader Documentation <https://instaloader.github.io/>`__
How to Automatically Download Pictures from Instagram
@@ -59,7 +64,7 @@ temporary directory, which will be reused later the next time ``--login``
is given. So you can download private profiles **non-interactively** when you
already have a valid session cookie file.
`Instaloader Documentation <https://instaloader.readthedocs.io/basic-usage.html>`__
`Instaloader Documentation <https://instaloader.github.io/basic-usage.html>`__
Disclaimer
+7 -1
View File
@@ -50,7 +50,7 @@ clean:
.PHONY: html
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
LC_ALL=C $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
@@ -223,3 +223,9 @@ dummy:
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
@echo
@echo "Build finished. Dummy builder generates no files."
favicon.ico: logo.svg
convert -density 384 -background transparent -define icon:auto-resize -colors 256 logo.svg favicon.ico
logo.png: logo.svg
inkscape -z -e logo.png -w 100 -h 100 logo.svg
+2 -2
View File
@@ -3,13 +3,13 @@
This directory contains sources for the Instaloader documentation.
The documentation is hosted at:
https://instaloader.readthedocs.io/
https://instaloader.github.io/
## Build
The documentation is created with [Sphinx](http://www.sphinx-doc.org/). To build it, use
```
pip3 install sphinx sphinx-autodoc-typehints
pip3 install -r requirements.txt
make html
```
+3
View File
@@ -0,0 +1,3 @@
code {
color: #222;
}
-1
View File
@@ -1 +0,0 @@
<h2>Instaloader</h2>
+8 -1
View File
@@ -1,4 +1,11 @@
{% extends "!layout.html" %}
{% block extrahead %}
<script async defer src="https://buttons.github.io/buttons.js"></script>
{{ super() }}
<meta name="google-site-verification" content="FNUsDdkOJM9obm9QEdrNBVd9AtAzHzA-aaSMCUrS6C8" />
{% if pagename == "index" %}
<link rel="canonical" href="https://instaloader.github.io/" />
{% else %}
<link rel="canonical" href="https://instaloader.github.io/{{ pagename }}.html" />
{% endif %}
<script async defer src="https://buttons.github.io/buttons.js"></script>
{% endblock %}
-18
View File
@@ -1,18 +0,0 @@
{% if next %}
<h3>Next</h3>
<ul>
<li class="toctree-l1"><a class="reference internal" href="{{ next.link|e }}">{{ next.title }}</a></li>
</ul>
{% endif %}
<h3>Links</h3>
<ul>
<li><a href="https://github.com/Thammus/instaloader/issues">Issue Tracker</a></li>
<li><a href="https://github.com/Thammus/instaloader/releases">Version History</a></li>
<li><a href="https://pypi.python.org/pypi/instaloader">Instaloader on PyPI</a></li>
<li><a href="https://github.com/Thammus/instaloader">Instaloader on GitHub</a></li>
</ul>
<ul>
<li>
<a class="github-button" href="https://github.com/Thammus/Instaloader" data-icon="octicon-star" data-size="large" data-show-count="true" aria-label="Star Thammus/Instaloader on GitHub">Star</a>
</li>
</ul>
+58
View File
@@ -0,0 +1,58 @@
<div id="navbar" class="{{ theme_navbar_class }} navbar-default {% if theme_navbar_fixed_top|tobool -%} navbar-fixed-top{%- endif -%}">
<div class="container">
<div class="navbar-header">
<!-- .btn-navbar is used as the toggle for collapsed navbar content -->
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{ pathto(master_doc) }}">
{%- block sidebarlogo %}
{%- if logo %}<span><img src="{{ pathto('_static/' + logo, 1) }}"></span>{%- endif %}
{%- endblock %}
{% if theme_navbar_title -%}{{ theme_navbar_title|e }}{%- else -%}{{ project|e }}{%- endif -%}
</a>
<span class="navbar-text navbar-version pull-left">
<a href="https://github.com/instaloader/instaloader/releases/tag/v{{current_release}}" title="Released on {{current_release_date}}.">v<b>{{current_release}}</b></a>
</span>
</div>
<div class="collapse navbar-collapse nav-collapse">
<ul class="nav navbar-nav">
{% if theme_navbar_links %}
{%- for link in theme_navbar_links %}
<li><a href="{{ pathto(*link[1:]) }}">{{ link[0] }}</a></li>
{%- endfor %}
{% endif %}
{% block navbartoc %}
{% include "globaltoc.html" %}
{% if theme_navbar_pagenav %}
{% include "navbartoc.html" %}
{% endif %}
{% endblock %}
{% if theme_navbar_sidebarrel %}
{% block sidebarrel %}
{% include "relations.html" %}
{% endblock %}
{% endif %}
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a role="button" id="dLabelUsefulLinks" data-toggle="dropdown" data-target="#" href="#">
Useful Links
<b class="caret"></b>
</a>
<ul class="dropdown-menu" role="menu" aria-labelledby="dLabelUsefulLinks">
<li><a href="https://github.com/instaloader/instaloader/issues">Issue Tracker</a></li>
<li><a href="https://github.com/instaloader/instaloader/releases">Version History</a></li>
<li><a href="https://pypi.python.org/pypi/instaloader">Instaloader on PyPI</a></li>
<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>
</ul>
</li>
</ul>
</div>
</div>
</div>
+10
View File
@@ -0,0 +1,10 @@
{% extends "!page.html" %}
{% block body %}
{{ super() }}
{% if next and prev %}
<h2>Next Section</h2>
<p>
<a class="reference internal" href="{{ next.link|e }}">{{ next.title }}</a>
</p>
{% endif %}
{% endblock %}
+46 -43
View File
@@ -1,3 +1,8 @@
.. meta::
:description:
Documentation of Instaloader module, a powerful and easy-to-use
Python library to download Instagram media and metadata.
Python Module :mod:`instaloader`
--------------------------------
@@ -5,68 +10,66 @@ Python Module :mod:`instaloader`
.. highlight:: python
You may also use parts of Instaloader as library to do other interesting
things.
Instaloader exposes its internally used methods as a Python module, making it a
**powerful and easy-to-use Python API for Instagram**, allowing to further
customize obtaining media and metadata.
For example, to get a list of all followees and a list of all followers of a profile, do
.. code:: python
Start with getting an instance of :class:`Instaloader`::
import instaloader
# Get instance
loader = instaloader.Instaloader()
L = instaloader.Instaloader()
# Login
loader.interactive_login(USERNAME)
# Optionally, login or load session
L.login(USER, PASSWORD) # (login)
L.interactive_login(USER) # (ask password on terminal)
L.load_session_from_file(USER) # (load session created w/
# `instaloader -l USERNAME`)
:mod:`instaloader` provides the :class:`Post` structure, which represents a
picture, video or sidecar (set of multiple pictures/videos) posted in a user's
profile. :class:`Instaloader` provides methods to iterate over Posts from a
certain source::
for post in L.get_hashtag_posts('cat'):
# post is an instance of instaloader.Post
L.download_post(post, target='#cat')
Besides :func:`Instaloader.get_hashtag_posts`, there is
:func:`Instaloader.get_feed_posts`, :func:`Instaloader.get_profile_posts` and
:func:`Instaloader.get_saved_posts`.
Also, :class:`Post` instances can be created with :func:`Post.from_shortcode`
and :func:`Post.from_mediaid`.
Further, information about profiles can be easily obtained. For example, you may
print a list of all followees and followers of a profile with::
# Print followees
print(PROFILE + " follows these profiles:")
for f in loader.get_followees(PROFILE):
for f in L.get_followees(PROFILE):
print("\t%s\t%s" % (f['username'], f['full_name']))
# Print followers
print("Followers of " + PROFILE + ":")
for f in loader.get_followers(PROFILE):
for f in L.get_followers(PROFILE):
print("\t%s\t%s" % (f['username'], f['full_name']))
Then, you may download all pictures of all followees with
Then, you may download all pictures of all followees with::
.. code:: python
for f in L.get_followees(PROFILE):
L.download_profile(f['username'])
for f in loader.get_followees(PROFILE):
loader.download_profile(f['username'])
Each Instagram profile has its own unique ID which stays unmodified even if a
user changes his/her username. To get said ID, given the profile's name, you may
call::
You could also download your last 20 liked pics with
.. code:: python
loader.download_feed_posts(max_count=20, fast_update=True,
filter_func=lambda post: post.viewer_has_liked)
To download the last 20 pictures with hashtag #cat, do
.. code:: python
loader.download_hashtag('cat', max_count=20)
Generally, :class:`Instaloader` provides methods to iterate over the Posts from
a certain source.
.. code:: python
for post in loader.get_hashtag_posts('cat'):
# post is an instance of instaloader.Post
loader.download_post(post, target='#cat')
Each Instagram profile has its own unique ID which stays unmodified even
if a user changes his/her username. To get said ID, given the profile's
name, you may call
.. code:: python
loader.get_id_by_username(PROFILE_NAME)
L.get_id_by_username(PROFILE_NAME)
A reference of the many methods provided by the :mod:`instaloader` module is
provided in the remainder of this document. Feel free to direct any issue or
contribution to
`Instaloader on Github <https://github.com/instaloader/instaloader>`__.
``Instaloader`` (Main Class)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+11 -1
View File
@@ -1,3 +1,10 @@
.. meta::
:description:
How to download pictures from Instagram. Description of basic
usage of Instaloader, free tool to download photos from public
and private profiles, hashtags, stories, feeds, saved media, and
their metadata, comments and captions.
Download Pictures from Instagram
---------------------------------
@@ -63,6 +70,9 @@ Instaloader supports the following targets:
``:feed``
Your **feed** (requires :option:`--login`),
``:saved``
Posts which are marked as **saved** (requires :option:`--login`),
``@profile``
All profiles that are followed by ``profile``, i.e. the *followees* of
``profile`` (requires :option:`--login`).
@@ -95,7 +105,7 @@ pattern, the token ``{target}`` is replaced by the target name, and
:option:`--filename-pattern` configures the path of the post's files relative
to the target directory. The default is ``--filename-pattern={date}``.
The tokens ``{target}`` and ``{profile}`` are replaced like in the
dirname pattern. Further, the tokens ``{date}`` and ``{shortcode}`` are
dirname pattern. Further, the tokens ``{date}``, ``{date_utc}`` and ``{shortcode}`` are
defined. Additionally, in case of not downloading stories, the attributes of
:class:`.Post` can be used, e.g. ``{post.owner_id}`` or ``{post.mediaid}``.
+27 -15
View File
@@ -1,33 +1,44 @@
Command Line Options
====================
The following flags can be given to Instaloader to specify how profiles should
be downloaded.
Instaloader is invoked with::
To get a list of all flags, their abbreviations and their descriptions,
run ``instaloader --help``.
$ instaloader [options] target [target ...]
where ``target`` is a ``profile``, a ``"#hashtag"``, ``@profile`` (all profiles
that *profile* is following), or if logged in ``:feed`` (pictures from your
feed), ``:stories`` (stories of your followees) or ``:saved`` (collection of
posts marked as saved).
Here we explain the additional options that can be given to Instaloader to
customize its behavior. To get a list of all flags, their abbreviations and
their descriptions, you may also run ``instaloader --help``.
What to Download
^^^^^^^^^^^^^^^^
Specify a list of profiles or #hashtags. For each of these, Instaloader
creates a folder and downloads all posts along with the pictures's
captions and the current **profile picture**. If an already-downloaded profile
has been renamed, Instaloader automatically **finds it by its unique ID** and
renames the folder likewise.
Instead of a *profile* or a *#hashtag*, the special targets
``:feed`` (pictures from your feed) and
``:stories`` (stories of your followees) can be specified.
Specify a list of targets (profiles, #hashtags, ``:feed``, ``:stories`` or
``:saved``). For each of these, Instaloader creates a folder and stores all
posts along with the pictures's captions and the current **profile picture**
there. If an already-downloaded profile has been renamed, Instaloader
automatically **finds it by its unique ID** and renames the folder likewise.
.. option:: --profile-pic-only
Only download profile picture.
.. option:: --no-profile-pic
Do not download profile picture.
.. option:: --no-videos
Do not download videos.
.. option:: --no-video-thumbnails
Do not download thumbnails of videos.
.. option:: --geotags
**Download geotags** when available. Geotags are stored as a text file with
@@ -66,7 +77,8 @@ Instead of a *profile* or a *#hashtag*, the special targets
.. option:: --stories-only
Rather than downloading regular posts of each specified profile, only
download stories. Requires :option:`--login`.
download stories. Requires :option:`--login`. Does not imply
:option:`--no-profile-pic`.
.. option:: --only-if filter
@@ -133,7 +145,7 @@ How to Download
Prefix of filenames. Posts are stored in the directory whose pattern is given
with ``--dirname-pattern``. ``{profile}`` is replaced by the profile name,
``{target}`` is replaced by the target you specified, i.e. either ``:feed``,
``#hashtag`` or the profile name. Also, the fields ``{date}`` and
``#hashtag`` or the profile name. Also, the fields ``{date}``, ``{date_utc}`` and
``{shortcode}`` can be specified. In case of not downloading stories, the
attributes of the :class:`.Post` class can be used in addition, e.g.
``{post.owner_id}`` or ``{post.mediaid}``.
+25 -7
View File
@@ -18,7 +18,11 @@
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import subprocess
import sys
import sphinx_bootstrap_theme
sys.path.insert(0, os.path.abspath('..'))
# -- General configuration ------------------------------------------------
@@ -132,16 +136,20 @@ todo_include_todos = False
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
html_theme = 'bootstrap'
html_theme_path = sphinx_bootstrap_theme.get_html_theme_path()
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#
html_theme_options = {
'show_powered_by': False,
'sidebar_width': '290px',
'page_width': '935px' }
'navbar_site_name': 'Site Contents',
'navbar_pagenav_name': 'Page Contents',
'navbar_pagenav': True,
'navbar_sidebarrel': True,
'nosidebar': True,
}
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
@@ -158,13 +166,13 @@ html_theme_options = {
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#
# html_logo = None
html_logo = "logo.png"
# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#
# html_favicon = None
html_favicon = "favicon.ico"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
@@ -190,7 +198,8 @@ html_static_path = ['_static']
# Custom sidebar templates, maps document names to template names.
#
html_sidebars = {'**': ["caption.html", "globaltoc.html", "relations.html", "links.html"] }
#html_sidebars = {'**': ["relations.html", "links.html"] }
html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
@@ -352,3 +361,12 @@ texinfo_documents = [
# If true, do not generate a @detailmenu in the "Top" node's menu.
#
# texinfo_no_detailmenu = False
current_release = subprocess.check_output(["git", "describe", "--abbrev=0"]).decode("ascii")[1:-1]
current_release_date = subprocess.check_output(["git", "log", "-1", "--tags", "--format=%ad", "--date=format:%e %b %Y"]).decode("ascii")[:-1]
html_context = {'current_release': current_release, 'current_release_date': current_release_date}
def setup(app):
app.add_stylesheet("style.css")
+83
View File
@@ -0,0 +1,83 @@
Contributing to Instaloader
===========================
As an open source project, Instaloader heavily depends on the contributions from
its community. In this document, we advise on how you may help Instaloader to
become an even greater tool.
Instaloader's development is organized on
`GitHub <https://github.com/instaloader/instaloader>`__, where Issues and Pull
Requests are discussed.
Reporting Bugs
--------------
If you encounter a bug, do not hesitate to report it in our
`Issue Tracker <https://github.com/instaloader/instaloader/issues>`__. Since
Instaloader is actively developed, the majority of bugs is fixed within only
**4 days** after being reported. When reporting a problem, please keep the
following in mind:
- Ensure you **use the latest version** of Instaloader. The currently-installed
version can be found out with ``instaloader --version``.
- 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.
- 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.).
- Include all **error messages and tracebacks** in the report.
- If not obvious, describe **which behavior you expected**
instead of what actually happened.
- If you think an issue has been closed accidentally or inappropriately, feel
free to reopen it.
Writing Code or Improving the Documentation
-------------------------------------------
Changes of the Instaloader source can be proposed as a
`Pull Request <https://github.com/instaloader/instaloader/pulls>`__. There are only
few things to consider:
- Sometimes, the most current code is not in the ``master`` branch. Check that
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
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``.
- 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
project.
Suggesting Features
-------------------
If you have an idea of how Instaloader should be enhanced, but do not want to
implement the feature yourself, feel free to open a ticket in our
`Issue Tracker <https://github.com/instaloader/instaloader/issues>`__.
Please consider the following:
- Instaloader already has plenty of features. **Check the documentation**
beforehand to ensure your desired suggestion is not already implemented.
- Briefly **ensure that your idea has not already been suggested**. If you find
an issue suggesting the same or a similar feature, share your thoughts in a
comment there, instead of opening a new issue.
- If possible, 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?
- **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.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

+23 -6
View File
@@ -1,6 +1,15 @@
.. meta::
:description:
Free command line tool to download photos from Instagram.
Scrapes public and private profiles, hashtags, stories, feeds,
saved media, and their metadata, comments and captions.
Written in Python.
Instaloader
===========
.. highlight:: none
**Instaloader** is a tool to download pictures (or videos) along with
their captions and other metadata from Instagram.
@@ -12,8 +21,8 @@ With `Python <https://www.python.org/>`__ installed, do::
**Instaloader**
- downloads **public and private profiles, hashtags, user stories and
feeds**,
- downloads **public and private profiles, hashtags, user stories,
feeds and saved media**,
- downloads **comments, geotags and captions** of each post,
@@ -23,9 +32,16 @@ With `Python <https://www.python.org/>`__ installed, do::
- allows **fine-grained customization** of filters and where to store
downloaded media,
- is free `open source <https://github.com/Thammus/instaloader>`__
- is free `open source <https://github.com/instaloader/instaloader>`__
software written in Python.
::
instaloader [--comments] [--geotags] [--stories]
[--login YOUR-USERNAME] [--fast-update]
profile | "#hashtag" |
:stories | :feed | :saved
Table of Contents
-----------------
@@ -37,14 +53,15 @@ Table of Contents
basic-usage
cli-options
as-module
contributing
Useful Links
------------
- `Git Repository (on GitHub) <https://github.com/Thammus/instaloader>`__
- `Git Repository (on GitHub) <https://github.com/instaloader/instaloader>`__
- `PyPI Project Page <https://pypi.python.org/pypi/instaloader>`__
- `Issue Tracker / Bug Tracker <https://github.com/Thammus/instaloader/issues>`__
- `Version History <https://github.com/Thammus/instaloader/releases>`__
- `Issue Tracker / Bug Tracker <https://github.com/instaloader/instaloader/issues>`__
- `Version History <https://github.com/instaloader/instaloader/releases>`__
.. include:: ../README.rst
:start-after: disclaimer-start
+17 -6
View File
@@ -12,11 +12,11 @@ installed, you may install Instaloader using
pip3 install instaloader
Alternatively, to get the most current version of Instaloader from our
`Git repository <https://github.com/Thammus/instaloader>`__:
`Git repository <https://github.com/instaloader/instaloader>`__:
::
pip3 install git+https://github.com/Thammus/instaloader
pip3 install git+https://github.com/instaloader/instaloader
(pass ``--upgrade`` to upgrade if Instaloader is already installed)
@@ -24,7 +24,18 @@ Instaloader requires
`requests <http://python-requests.org/>`__, which
will be installed automatically, if it is not already installed.
If you do not want to use pip, even though it is highly recommended,
and prefer installing Instaloader manually,
`Download the Source <https://github.com/Thammus/instaloader/releases>`__,
extract the Zip or Tarball and execute ``instaloader.py`` from there.
**Alternative methods for installing Instaloader:**
- If you do not want to use pip, even though it is highly recommended,
and prefer to **install Instaloader manually**,
`Download the Source <https://github.com/instaloader/instaloader/releases/latest>`__,
extract the Zip or Tarball and execute ``instaloader.py`` from there.
- On **Arch Linux**, you may install Instaloader using the
`Instaloader AUR package <https://aur.archlinux.org/packages/instaloader/>`__.
- On **Gentoo Linux**, you may install Instaloader using the
`Instaloader Ebuild <https://bugs.gentoo.org/642100>`__.
- On **Windows 10**, you may download the standalone executable from the
`current release page <https://github.com/instaloader/instaloader/releases/latest>`__.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

+117
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

+3
View File
@@ -1 +1,4 @@
requests
sphinx
sphinx-autodoc-typehints
sphinx-bootstrap-theme
+371 -152
View File
@@ -6,6 +6,7 @@ import getpass
import json
import os
import pickle
import platform
import random
import re
import shutil
@@ -22,15 +23,21 @@ from datetime import datetime
from enum import Enum
from io import BytesIO
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
import requests
import requests.utils
import urllib3
__version__ = '3.2'
__version__ = '3.3.4'
# NOTE: duplicated in README.rst and docs/index.rst
USAGE_STRING = """
{0} [--comments] [--geotags] [--stories]
{2:{1}} [--login YOUR-USERNAME] [--fast-update]
{2:{1}} profile | "#hashtag" | :stories | :feed | :saved
{0} --help""".format(sys.argv[0], len(sys.argv[0]), '')
try:
# pylint:disable=wrong-import-position
@@ -53,6 +60,10 @@ class QueryReturnedNotFoundException(InstaloaderException):
pass
class QueryReturnedForbiddenException(InstaloaderException):
pass
class ProfileNotExistsException(InstaloaderException):
pass
@@ -123,10 +134,10 @@ def mediaid_to_shortcode(mediaid: int) -> str:
return b64encode(mediaid.to_bytes(9, 'big'), b'-_').decode().replace('A', ' ').lstrip().replace(' ','A')
def format_string_contains_key(format_string: str, key: str) -> bool:
def format_string_contains_key(format_string: '_PathPattern', key: str) -> bool:
# pylint:disable=unused-variable
for literal_text, field_name, format_spec, conversion in string.Formatter().parse(format_string):
if field_name == key or field_name.startswith(key + '.'):
if field_name and (field_name == key or field_name.startswith(key + '.')):
return True
return False
@@ -165,7 +176,8 @@ class Post:
"""
Structure containing information about an Instagram post.
Created by Instaloader methods :meth:`.get_profile_posts`, :meth:`.get_hashtag_posts`, :meth:`.get_feed_posts`.
Created by Instaloader methods :meth:`.get_profile_posts`, :meth:`.get_hashtag_posts`, :meth:`.get_feed_posts` and
:meth:`.get_saved_posts`.
Posts are linked to an :class:`Instaloader` instance which is used for error logging and obtaining of additional
metadata, if required. This class unifies access to the properties associated with a post. It implements == and is
hashable.
@@ -267,10 +279,15 @@ class Post:
return int(self._field('owner', 'id'))
@property
def date(self) -> datetime:
"""Timestamp when the post was created."""
def date_local(self) -> datetime:
"""Timestamp when the post was created (local time zone)."""
return datetime.fromtimestamp(self._node["date"] if "date" in self._node else self._node["taken_at_timestamp"])
@property
def date_utc(self) -> datetime:
"""Timestamp when the post was created (UTC)."""
return datetime.utcfromtimestamp(self._node["date"] if "date" in self._node else self._node["taken_at_timestamp"])
@property
def url(self) -> str:
"""URL of the picture / video thumbnail of the post"""
@@ -355,7 +372,11 @@ class Post:
return self._field('edge_media_to_comment', 'count')
def get_comments(self) -> Iterator[Dict[str, Any]]:
"""Iterate over all comments of the post."""
"""Iterate over all comments of the post.
Each comment is represented by a dictionary having the keys text, created_at, id and owner, which is a
dictionary with keys username, profile_pic_url and id.
"""
if self.comments == 0:
# Avoid doing additional requests if there are no comments
return
@@ -367,13 +388,30 @@ class Post:
'https://www.instagram.com/p/' + self.shortcode + '/',
lambda d: d['data']['shortcode_media']['edge_media_to_comment'])
def get_likes(self) -> Iterator[Dict[str, Any]]:
"""Iterate over all likes of the post.
Each like is represented by a dictionary having the keys username, followed_by_viewer, id, is_verified,
requested_by_viewer, followed_by_viewer, profile_pic_url.
"""
if self.likes == 0:
# Avoid doing additional requests if there are no comments
return
likes_edges = self._field('edge_media_preview_like', 'edges')
if self.likes == len(likes_edges):
# If the Post's metadata already contains all likes, don't do GraphQL requests to obtain them
yield from (like['node'] for like in likes_edges)
yield from self._instaloader.graphql_node_list("1cb6ec562846122743b61e492c85999f", {'shortcode': self.shortcode},
'https://www.instagram.com/p/' + self.shortcode + '/',
lambda d: d['data']['shortcode_media']['edge_liked_by'])
def get_location(self) -> Optional[Dict[str, str]]:
"""If the Post has a location, returns a dictionary with fields 'lat' and 'lng'."""
loc_dict = self._field("location")
if loc_dict is not None:
location_json = self._instaloader.get_json("explore/locations/{0}/".format(loc_dict["id"]),
params={'__a': 1})
return location_json["location"]
return location_json["location"] if "location" in location_json else location_json['graphql']['location']
@staticmethod
def json_encoder(obj) -> Dict[str, Any]:
@@ -409,13 +447,24 @@ class Tristate(Enum):
always = 2
class _PathPattern(str):
"""Class overriding :meth:`str.format` for character substitution in paths for Windows, see issue #84."""
def format(self, *args: Any, **kwargs: Any) -> str:
ret = super().format(*args, **kwargs)
return ret.replace(':', '\ua789') if platform.system() == 'Windows' else ret
class Instaloader:
GRAPHQL_PAGE_LENGTH = 200
def __init__(self,
sleep: bool = True, quiet: bool = False,
user_agent: Optional[str] = None,
dirname_pattern: Optional[str] = None,
filename_pattern: Optional[str] = None,
download_videos: Tristate = Tristate.always,
download_video_thumbnails: Tristate = Tristate.always,
download_geotags: Tristate = Tristate.no_extra_query,
save_captions: Tristate = Tristate.no_extra_query,
download_comments: Tristate = Tristate.no_extra_query,
@@ -428,10 +477,21 @@ class Instaloader:
self.username = None
self.sleep = sleep
self.quiet = quiet
self.dirname_pattern = dirname_pattern if dirname_pattern is not None else '{target}'
self.filename_pattern = filename_pattern.replace('{date}', '{date:%Y-%m-%d_%H-%M-%S}') \
if filename_pattern is not None else '{date:%Y-%m-%d_%H-%M-%S}'
self.dirname_pattern = _PathPattern(dirname_pattern if dirname_pattern is not None else '{target}')
if filename_pattern is not None:
filename_pattern = re.sub(r"({(?:post\.)?date)([:}])", r"\1_utc\2", filename_pattern)
self.filename_pattern_old = filename_pattern.replace('{date_utc}', '{date_utc:%Y-%m-%d_%H-%M-%S}')
self.filename_pattern_old = _PathPattern(re.sub(r"(?i)({(?:post\.)?date_utc:[^}]*?)_UTC",
r"\1", self.filename_pattern_old))
filename_pattern = re.sub(r"(?i)({(date_utc|post\.date_utc):(?![^}]*UTC[^}]*).*?)}",
r"\1_UTC}", filename_pattern)
self.filename_pattern = _PathPattern(filename_pattern.replace('{date_utc}',
'{date_utc:%Y-%m-%d_%H-%M-%S_UTC}'))
else:
self.filename_pattern = _PathPattern('{date_utc:%Y-%m-%d_%H-%M-%S_UTC}')
self.filename_pattern_old = _PathPattern('{date_utc:%Y-%m-%d_%H-%M-%S}')
self.download_videos = download_videos
self.download_video_thumbnails = download_video_thumbnails
self.download_geotags = download_geotags
self.save_captions = save_captions
self.download_comments = download_comments
@@ -454,7 +514,9 @@ class Instaloader:
"""Yield an anonymous, otherwise equally-configured copy of an Instaloader instance; Then copy its error log."""
new_loader = Instaloader(self.sleep, self.quiet, self.user_agent,
self.dirname_pattern, self.filename_pattern,
self.download_videos, self.download_geotags,
self.download_videos,
self.download_video_thumbnails,
self.download_geotags,
self.save_captions, self.download_comments,
self.save_metadata, self.max_connection_attempts)
new_loader.previous_queries = self.previous_queries
@@ -507,6 +569,9 @@ class Instaloader:
resp.raw.decode_content = True
shutil.copyfileobj(resp.raw, file)
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))
@@ -523,12 +588,13 @@ class Instaloader:
self.error("[skipped by user]", repeat_at_end=False)
raise ConnectionException(error_string)
def get_json(self, url: str, params: Dict[str, Any],
def get_json(self, path: str, params: Dict[str, Any], host: str = 'www.instagram.com',
session: Optional[requests.Session] = None, _attempt = 1) -> Dict[str, Any]:
"""JSON request to Instagram.
:param url: URL, relative to www.instagram.com/
:param path: URL, relative to the given domain which defaults to www.instagram.com/
:param params: GET parameters
:param host: Domain part of the URL from where to download the requested JSON; defaults to www.instagram.com
:param session: Session to use, or None to use self.session
:return: Decoded response dictionary
:raises QueryReturnedNotFoundException: When the server responds with a 404.
@@ -545,7 +611,7 @@ class Instaloader:
if len(timestamps) < 100 and not untracked_queries:
return 0
return round(min(timestamps) + sliding_window - current_time) + 6
is_graphql_query = 'query_id' in params and 'graphql/query' in url
is_graphql_query = 'query_id' in params and 'graphql/query' in path
if is_graphql_query:
query_id = params['query_id']
waittime = graphql_query_waittime(query_id)
@@ -560,7 +626,7 @@ class Instaloader:
sess = session if session else self.session
try:
self._sleep()
resp = sess.get('https://www.instagram.com/' + url, params=params)
resp = sess.get('https://{0}/{1}'.format(host, path), params=params)
if resp.status_code == 404:
raise QueryReturnedNotFoundException("404")
if resp.status_code == 429:
@@ -576,7 +642,7 @@ class Instaloader:
raise ConnectionException("Returned \"{}\" status.".format(resp_json['status']))
return resp_json
except (ConnectionException, json.decoder.JSONDecodeError, requests.exceptions.RequestException) as err:
error_string = "JSON Query to {}: {}".format(url, err)
error_string = "JSON Query to {}: {}".format(path, err)
if _attempt == self.max_connection_attempts:
raise ConnectionException(error_string)
self.error(error_string + " [retrying; skip with ^C]", repeat_at_end=False)
@@ -592,7 +658,7 @@ class Instaloader:
self._log('The request will be retried in {} seconds.'.format(waittime))
time.sleep(waittime)
self._sleep()
return self.get_json(url, params, sess, _attempt + 1)
return self.get_json(path=path, params=params, host=host, session=sess, _attempt=_attempt + 1)
except KeyboardInterrupt:
self.error("[skipped by user]", repeat_at_end=False)
raise ConnectionException(error_string)
@@ -626,12 +692,12 @@ class Instaloader:
session.headers.update(self._default_http_header(empty_session_only=True))
return session
def graphql_query(self, query_id: int, variables: Dict[str, Any],
def graphql_query(self, query_identifier: Union[int, str], variables: Dict[str, Any],
referer: Optional[str] = None) -> Dict[str, Any]:
"""
Do a GraphQL Query.
:param query_id: Query ID.
:param query_identifier: Query ID or Hash.
:param variables: Variables for the Query.
:param referer: HTTP Referer, or None.
:return: The server's response dictionary.
@@ -645,8 +711,9 @@ class Instaloader:
tmpsession.headers['accept'] = '*/*'
if referer is not None:
tmpsession.headers['referer'] = urllib.parse.quote(referer)
resp_json = self.get_json('graphql/query', params={'query_id': query_id,
'variables': json.dumps(variables, separators=(',', ':'))},
resp_json = self.get_json('graphql/query',
params={'query_id' if isinstance(query_identifier, int) else 'query_hash': query_identifier,
'variables': json.dumps(variables, separators=(',', ':'))},
session=tmpsession)
if 'status' not in resp_json:
self.error("GraphQL response did not contain a \"status\" field.")
@@ -673,17 +740,18 @@ class Instaloader:
his/her username. To get said ID, given the profile's name, you may call this function."""
return int(self.get_profile_metadata(profile)['user']['id'])
def graphql_node_list(self, query_id: int, query_variables: Dict[str, Any], query_referer: Optional[str],
def graphql_node_list(self, query_identifier: Union[int, str], query_variables: Dict[str, Any],
query_referer: Optional[str],
edge_extractor: Callable[[Dict[str, Any]], Dict[str, Any]]) -> Iterator[Dict[str, Any]]:
"""Retrieve a list of GraphQL nodes."""
query_variables['first'] = 500
data = self.graphql_query(query_id, query_variables, query_referer)
query_variables['first'] = Instaloader.GRAPHQL_PAGE_LENGTH
data = self.graphql_query(query_identifier, query_variables, query_referer)
while True:
edge_struct = edge_extractor(data)
yield from [edge['node'] for edge in edge_struct['edges']]
if edge_struct['page_info']['has_next_page']:
query_variables['after'] = edge_struct['page_info']['end_cursor']
data = self.graphql_query(query_id, query_variables, query_referer)
data = self.graphql_query(query_identifier, query_variables, query_referer)
else:
break
@@ -712,17 +780,24 @@ class Instaloader:
lambda d: d['data']['user']['edge_follow'])
def download_pic(self, filename: str, url: str, mtime: datetime,
filename_suffix: Optional[str] = None) -> bool:
filename_alt: Optional[str] = None, filename_suffix: Optional[str] = None) -> bool:
"""Downloads and saves picture with given url under given directory with given timestamp.
Returns true, if file was actually downloaded, i.e. updated."""
urlmatch = re.search('\\.[a-z0-9]*\\?', url)
file_extension = url[-3:] if urlmatch is None else urlmatch.group(0)[1:-1]
if filename_suffix is not None:
filename += '_' + filename_suffix
if filename_alt is not None:
filename_alt += '_' + filename_suffix
filename += '.' + file_extension
if os.path.isfile(filename):
self._log(filename + ' exists', end=' ', flush=True)
return False
if filename_alt is not None:
filename_alt += '.' + file_extension
if os.path.isfile(filename_alt):
self._log(filename_alt + 'exists', end=' ', flush=True)
return False
self._get_and_write_raw(url, filename)
os.utime(filename, (datetime.now().timestamp(), mtime.timestamp()))
return True
@@ -733,15 +808,20 @@ class Instaloader:
json.dump(post, fp=open(filename, 'w'), indent=4, default=Post.json_encoder)
self._log('json', end=' ', flush=True)
def update_comments(self, filename: str, post: Post) -> None:
filename += '_comments.json'
def update_comments(self, filename: str, post: Post, filename_alt: Optional[str] = None) -> None:
try:
comments = json.load(open(filename))
filename_current = filename + '_comments.json'
comments = json.load(open(filename_current))
except FileNotFoundError:
comments = list()
try:
filename_current = filename_alt + '_comments.json'
comments = json.load(open(filename_current))
except (FileNotFoundError, TypeError):
filename_current = filename + '_comments.json'
comments = list()
comments.extend(post.get_comments())
if comments:
with open(filename, 'w') as file:
with open(filename_current, 'w') as file:
comments_list = sorted(sorted(list(comments), key=lambda t: t['id']),
key=lambda t: t['created_at'], reverse=True)
unique_comments_list = [comments_list[0]]
@@ -754,17 +834,25 @@ class Instaloader:
if x['id'] != y['id']:
unique_comments_list.append(y)
file.write(json.dumps(unique_comments_list, indent=4))
os.rename(filename_current, filename + '_comments.json')
self._log('comments', end=' ', flush=True)
def save_caption(self, filename: str, mtime: datetime, caption: str) -> None:
def save_caption(self, filename: str, mtime: datetime, caption: str, filename_alt: Optional[str] = None) -> None:
"""Updates picture caption"""
filename += '.txt'
if filename_alt is not None:
filename_alt += '.txt'
pcaption = caption.replace('\n', ' ').strip()
caption = caption.encode("UTF-8")
pcaption = '[' + ((pcaption[:29] + u"\u2026") if len(pcaption) > 31 else pcaption) + ']'
with suppress(FileNotFoundError):
with open(filename, 'rb') as file:
file_caption = file.read()
try:
with open(filename, 'rb') as file:
file_caption = file.read()
except FileNotFoundError:
if filename_alt is not None:
with open(filename_alt, 'rb') as file:
file_caption = file.read()
if file_caption.replace(b'\r\n', b'\n') == caption.replace(b'\r\n', b'\n'):
try:
self._log(pcaption + ' unchanged', end=' ', flush=True)
@@ -772,15 +860,22 @@ class Instaloader:
self._log('txt unchanged', end=' ', flush=True)
return None
else:
def get_filename(index):
return filename if index == 0 else (filename[:-4] + '_old_' +
(str(0) if index < 10 else str()) + str(index) + filename[-4:])
def get_filename(file, index):
return file if index == 0 else (file[:-4] + '_old_' +
(str(0) if index < 10 else str()) + str(index) + file[-4:])
i = 0
while os.path.isfile(get_filename(i)):
file_exists_list = []
while True:
file_exists_list.append(1 if os.path.isfile(get_filename(filename, i)) else 0)
if not file_exists_list[i] and filename_alt is not None:
file_exists_list[i] = 2 if os.path.isfile(get_filename(filename_alt, i)) else 0
if not file_exists_list[i]:
break
i = i + 1
for index in range(i, 0, -1):
os.rename(get_filename(index - 1), get_filename(index))
os.rename(get_filename(filename if file_exists_list[index - 1] % 2 else filename_alt, index - 1),
get_filename(filename, index))
try:
self._log(pcaption + ' updated', end=' ', flush=True)
except UnicodeEncodeError:
@@ -803,8 +898,17 @@ class Instaloader:
os.utime(filename, (datetime.now().timestamp(), mtime.timestamp()))
self._log('geo', end=' ', flush=True)
def download_profilepic(self, name: str, url: str) -> None:
"""Downloads and saves profile pic with given url."""
def download_profilepic(self, name: str, profile_metadata: Dict[str, Any]) -> None:
"""Downloads and saves profile pic."""
try:
data = self.get_json(path='api/v1/users/{0}/info/'.format(profile_metadata["user"]["id"]), params={},
host='i.instagram.com', session=self._get_anonymous_session())
url = data["user"]["hd_profile_pic_url_info"]["url"]
except (InstaloaderException, KeyError) as err:
self.error('{} Unable to fetch high quality profile pic.'.format(err))
url = profile_metadata["user"]["profile_pic_url_hd"] if "profile_pic_url_hd" in profile_metadata["user"] \
else profile_metadata["user"]["profile_pic_url"]
def _epoch_to_string(epoch: datetime) -> str:
return epoch.strftime('%Y-%m-%d_%H-%M-%S')
@@ -822,7 +926,6 @@ class Instaloader:
if os.path.isfile(filename):
self._log(filename + ' already exists')
return None
url = re.sub(r'/s([1-9][0-9]{2})x\1/', '/s2048x2048/', url)
self._get_and_write_raw(url, filename)
os.utime(filename, (datetime.now().timestamp(), date_object.timestamp()))
self._log('') # log output of _get_and_write_raw() does not produce \n
@@ -858,11 +961,10 @@ class Instaloader:
self.session = session
self.username = username
def test_login(self, session: Optional[requests.Session]) -> Optional[str]:
def test_login(self) -> Optional[str]:
"""Returns the Instagram username to which given :class:`requests.Session` object belongs, or None."""
if session:
data = self.get_json('', params={'__a': 1}, session=session)
return data['graphql']['user']['username'] if 'graphql' in data else None
data = self.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {})
return data["data"]["user"]["username"] if data["data"]["user"] is not None else None
def login(self, user: str, passwd: str) -> None:
"""Log in to instagram with given username and password and internally store session object"""
@@ -879,10 +981,12 @@ class Instaloader:
data={'password': passwd, 'username': user}, allow_redirects=True)
session.headers.update({'X-CSRFToken': login.cookies['csrftoken']})
if login.status_code == 200:
if user == self.test_login(session):
self.session = session
if user == self.test_login():
self.username = user
self.session = session
else:
self.username = None
self.session = None
raise BadCredentialsException('Login error! Check your credentials!')
else:
raise ConnectionException('Login error! Connection error!')
@@ -903,53 +1007,68 @@ class Instaloader:
profilename = post.owner_username if needs_profilename else None
dirname = self.dirname_pattern.format(profile=profilename, target=target.lower())
filename = dirname + '/' + self.filename_pattern.format(profile=profilename, target=target.lower(),
date=post.date, shortcode=post.shortcode,
date_utc=post.date_utc,
shortcode=post.shortcode,
post=post)
filename_old = dirname + '/' + self.filename_pattern_old.replace("{post.date_utc", "{date_utc") \
.format(profile=profilename, target=target.lower(),
date_utc=post.date_local,
shortcode=post.shortcode,
post=post)
os.makedirs(os.path.dirname(filename), exist_ok=True)
# Download the image(s) / video thumbnail and videos within sidecars if desired
downloaded = False
if post.typename == 'GraphSidecar':
edge_number = 1
downloaded = True
for edge in post.get_sidecar_edges():
edge_downloaded = self.download_pic(filename=filename,
# Download picture or video thumbnail
if not edge['node']['is_video'] or self.download_video_thumbnails is Tristate.always:
downloaded |= self.download_pic(filename=filename,
filename_alt=filename_old,
url=edge['node']['display_url'],
mtime=post.date,
mtime=post.date_local,
filename_suffix=str(edge_number))
# Additionally download video if available and desired
if edge['node']['is_video'] and self.download_videos is Tristate.always:
self.download_pic(filename=filename,
url=edge['node']['video_url'],
mtime=post.date,
filename_suffix=str(edge_number))
downloaded = downloaded and edge_downloaded
downloaded |= self.download_pic(filename=filename,
filename_alt=filename_old,
url=edge['node']['video_url'],
mtime=post.date_local,
filename_suffix=str(edge_number))
edge_number += 1
elif post.typename in ['GraphImage', 'GraphVideo']:
downloaded = self.download_pic(filename=filename, url=post.url, mtime=post.date)
elif post.typename == 'GraphImage':
downloaded = self.download_pic(filename=filename, filename_alt=filename_old,
url=post.url, mtime=post.date_local)
elif post.typename == 'GraphVideo':
if self.download_video_thumbnails is Tristate.always:
downloaded = self.download_pic(filename=filename, filename_alt=filename_old,
url=post.url, mtime=post.date_local)
else:
self.error("Warning: {0} has unknown typename: {1}".format(post, post.typename))
downloaded = False
# Save caption if desired
if self.save_captions is not Tristate.never:
if post.caption:
self.save_caption(filename, post.date, post.caption)
self.save_caption(filename=filename, filename_alt=filename_old,
mtime=post.date_local, caption=post.caption)
else:
self._log("<no caption>", end=' ', flush=True)
# Download video if desired
if post.is_video and self.download_videos is Tristate.always:
self.download_pic(filename=filename, url=post.video_url, mtime=post.date)
downloaded |= self.download_pic(filename=filename, filename_alt=filename_old,
url=post.video_url, mtime=post.date_local)
# Download geotags if desired
if self.download_geotags is Tristate.always:
location = post.get_location()
if location:
self.save_location(filename, location, post.date)
self.save_location(filename, location, post.date_local)
# Update comments if desired
if self.download_comments is Tristate.always:
self.update_comments(filename, post)
self.update_comments(filename=filename, filename_alt=filename_old, post=post)
# Save metadata as JSON if desired. It might require an extra query, depending on which information has been
# already obtained. Regarding Tristate interpretation, we always assume that it requires an extra query.
@@ -1025,71 +1144,80 @@ class Instaloader:
for item in user_stories["items"]:
self._log("[%3i/%3i] " % (count, totalcount), end="", flush=True)
count += 1
shortcode = item["code"] if "code" in item else "no_code"
date = datetime.fromtimestamp(item["taken_at"])
dirname = self.dirname_pattern.format(profile=name, target=filename_target)
filename = dirname + '/' + self.filename_pattern.format(profile=name, target=filename_target,
date=date,
shortcode=shortcode)
os.makedirs(os.path.dirname(filename), exist_ok=True)
with self._error_catcher('Download story {} from user {}'.format(shortcode, name)):
if "image_versions2" in item:
url = item["image_versions2"]["candidates"][0]["url"]
downloaded = self.download_pic(filename=filename,
url=url,
mtime=date)
else:
self._log("Warning: Unable to find story image.")
downloaded = False
if "caption" in item and item["caption"] is not None and \
self.save_captions is not Tristate.never:
caption = item["caption"]
if isinstance(caption, dict) and "text" in caption:
caption = caption["text"]
self.save_caption(filename, date, caption)
else:
self._log("<no caption>", end=' ', flush=True)
if "video_versions" in item and self.download_videos is Tristate.always:
downloaded = self.download_pic(filename=filename,
url=item["video_versions"][0]["url"],
mtime=date)
if item["story_locations"] and self.download_geotags is not Tristate.never:
location = item["story_locations"][0]["location"]
if location:
self.save_location(filename, location, date)
self._log()
with self._error_catcher('Download story from user {}'.format(name)):
downloaded = self.download_story(item, filename_target, name)
if fast_update and not downloaded:
break
def download_story(self, item: Dict[str, Any], target: str, profile: str) -> bool:
"""Download one user story.
:param item: Story item, as in story['items'] for story in :meth:`get_stories`
:param target: Replacement for {target} in dirname_pattern and filename_pattern
:param profile: Owner profile name
:return: True if something was downloaded, False otherwise, i.e. file was already there
"""
shortcode = item["code"] if "code" in item else "no_code"
date_local = datetime.fromtimestamp(item["taken_at"])
date_utc = datetime.utcfromtimestamp(item["taken_at"])
dirname = self.dirname_pattern.format(profile=profile, target=target)
filename = dirname + '/' + self.filename_pattern.format(profile=profile, target=target,
date_utc=date_utc,
shortcode=shortcode)
filename_old = dirname + '/' + self.filename_pattern_old.format(profile=profile, target=target,
date_utc=date_local,
shortcode=shortcode)
os.makedirs(os.path.dirname(filename), exist_ok=True)
downloaded = False
if "image_versions2" in item:
if "video_versions" not in item or self.download_video_thumbnails is Tristate.always:
url = item["image_versions2"]["candidates"][0]["url"]
downloaded = self.download_pic(filename=filename,
filename_alt=filename_old,
url=url,
mtime=date_local)
else:
self._log("Warning: Unable to find story image.")
if "caption" in item and item["caption"] is not None and \
self.save_captions is not Tristate.never:
caption = item["caption"]
if isinstance(caption, dict) and "text" in caption:
caption = caption["text"]
self.save_caption(filename=filename, filename_alt=filename_old, mtime=date_local, caption=caption)
else:
self._log("<no caption>", end=' ', flush=True)
if "video_versions" in item and self.download_videos is Tristate.always:
downloaded |= self.download_pic(filename=filename,
filename_alt=filename_old,
url=item["video_versions"][0]["url"],
mtime=date_local)
if item["story_locations"] and self.download_geotags is not Tristate.never:
location = item["story_locations"][0]["location"]
if location:
self.save_location(filename, location, date_local)
self._log()
return downloaded
def get_feed_posts(self) -> Iterator[Post]:
"""Get Posts of the user's feed."""
data = self.get_json('', params={'__a': 1})
if not self.is_logged_in:
return
data = self.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {})["data"]
while True:
if "graphql" in data:
is_edge = True
feed = data["graphql"]["user"]["edge_web_feed_timeline"]
elif "data" in data:
is_edge = True
feed = data["data"]["user"]["edge_web_feed_timeline"]
else:
is_edge = False
feed = data["feed"]["media"]
if is_edge:
yield from (Post(self, edge["node"]) for edge in feed["edges"])
else:
yield from (Post(self, node) for node in feed["nodes"])
feed = data["user"]["edge_web_feed_timeline"]
yield from (Post(self, edge["node"]) for edge in feed["edges"]
if not edge["node"]["__typename"] == "GraphSuggestedUserFeedUnit")
if not feed["page_info"]["has_next_page"]:
break
data = self.graphql_query(17863003771166879, {'fetch_media_item_count': 12,
'fetch_media_item_cursor': feed["page_info"]["end_cursor"],
'fetch_comment_count': 4,
'fetch_like': 10})
data = self.graphql_query("d6f4427fbe92d846298cf93df0b937d3",
{'fetch_media_item_count': 12,
'fetch_media_item_cursor': feed["page_info"]["end_cursor"],
'fetch_comment_count': 4,
'fetch_like': 10,
'has_stories': False})["data"]
def download_feed_posts(self, max_count: int = None, fast_update: bool = False,
filter_func: Optional[Callable[[Post], bool]] = None) -> None:
@@ -1122,6 +1250,56 @@ class Instaloader:
if fast_update and not downloaded:
break
def get_saved_posts(self) -> Iterator[Post]:
"""Get Posts that are marked as saved by the user."""
if not self.is_logged_in:
return
data = self.get_profile_metadata(self.username)
user_id = data["user"]["id"]
while True:
if "edge_saved_media" in data["user"]:
is_edge = True
saved_media = data["user"]["edge_saved_media"]
else:
is_edge = False
saved_media = data["user"]["saved_media"]
if is_edge:
yield from (Post(self, edge["node"]) for edge in saved_media["edges"])
else:
yield from (Post(self, node) for node in saved_media["nodes"])
if not saved_media["page_info"]["has_next_page"]:
break
data = self.graphql_query("f883d95537fbcd400f466f63d42bd8a1",
{'id': user_id, 'first': Instaloader.GRAPHQL_PAGE_LENGTH,
'after': saved_media["page_info"]["end_cursor"]})['data']
def download_saved_posts(self, max_count: int = None, fast_update: bool = False,
filter_func: Optional[Callable[[Post], bool]] = None) -> None:
"""Download user's saved pictures.
:param max_count: Maximum count of pictures to download
:param fast_update: If true, abort when first already-downloaded picture is encountered
:param filter_func: function(post), which returns True if given picture should be downloaded
"""
count = 1
for post in self.get_saved_posts():
if max_count is not None and count > max_count:
break
name = post.owner_username
if filter_func is not None and not filter_func(post):
self._log("<pic by {} skipped".format(name), flush=True)
continue
self._log("[{:>3}] {} ".format(count, name), end=str(), flush=True)
count += 1
with self._error_catcher('Download saved posts'):
downloaded = self.download_post(post, target=':saved')
if fast_update and not downloaded:
break
def get_hashtag_posts(self, hashtag: str) -> Iterator[Post]:
"""Get Posts associated with a #hashtag."""
yield from (Post(self, node) for node in
@@ -1214,7 +1392,8 @@ class Instaloader:
def get_profile_metadata(self, profile_name: str) -> Dict[str, Any]:
"""Retrieves a profile's metadata, for use with e.g. :meth:`get_profile_posts` and :meth:`check_profile_id`."""
try:
return self.get_json('{}/'.format(profile_name), params={'__a': 1})
metadata = self.get_json('{}/'.format(profile_name), params={'__a': 1})
return metadata['graphql'] if 'graphql' in metadata else metadata
except QueryReturnedNotFoundException:
raise ProfileNotExistsException('Profile {} does not exist.'.format(profile_name))
@@ -1222,15 +1401,22 @@ class Instaloader:
"""Retrieve all posts from a profile."""
profile_name = profile_metadata['user']['username']
profile_id = int(profile_metadata['user']['id'])
yield from (Post(self, node, profile=profile_name, profile_id=profile_id)
for node in profile_metadata['user']['media']['nodes'])
has_next_page = profile_metadata['user']['media']['page_info']['has_next_page']
end_cursor = profile_metadata['user']['media']['page_info']['end_cursor']
if 'media' in profile_metadata['user']:
# backwards compatibility with old non-graphql structure
yield from (Post(self, node, profile=profile_name, profile_id=profile_id)
for node in profile_metadata['user']['media']['nodes'])
has_next_page = profile_metadata['user']['media']['page_info']['has_next_page']
end_cursor = profile_metadata['user']['media']['page_info']['end_cursor']
else:
yield from (Post(self, edge['node'], profile=profile_name, profile_id=profile_id)
for edge in profile_metadata['user']['edge_owner_to_timeline_media']['edges'])
has_next_page = profile_metadata['user']['edge_owner_to_timeline_media']['page_info']['has_next_page']
end_cursor = profile_metadata['user']['edge_owner_to_timeline_media']['page_info']['end_cursor']
while has_next_page:
# We do not use self.graphql_node_list() here, because profile_metadata
# lets us obtain the first 12 nodes 'for free'
data = self.graphql_query(17888483320059182, {'id': profile_metadata['user']['id'],
'first': 500,
'first': Instaloader.GRAPHQL_PAGE_LENGTH,
'after': end_cursor},
'https://www.instagram.com/{0}/'.format(profile_name))
media = data['data']['user']['edge_owner_to_timeline_media']
@@ -1240,7 +1426,8 @@ class Instaloader:
end_cursor = media['page_info']['end_cursor']
def download_profile(self, name: str,
profile_pic_only: bool = False, fast_update: bool = False,
profile_pic: bool = True, profile_pic_only: bool = False,
fast_update: bool = False,
download_stories: bool = False, download_stories_only: bool = False,
filter_func: Optional[Callable[[Post], bool]] = None) -> None:
"""Download one profile"""
@@ -1261,8 +1448,9 @@ class Instaloader:
profile_metadata = self.get_profile_metadata(name)
# Download profile picture
with self._error_catcher('Download profile picture of {}'.format(name)):
self.download_profilepic(name, profile_metadata["user"]["profile_pic_url"])
if profile_pic or profile_pic_only:
with self._error_catcher('Download profile picture of {}'.format(name)):
self.download_profilepic(name, profile_metadata)
if profile_pic_only:
return
@@ -1284,13 +1472,13 @@ class Instaloader:
if download_stories_only:
return
if ("nodes" not in profile_metadata["user"]["media"] or
not profile_metadata["user"]["media"]["nodes"]):
raise ProfileHasNoPicsException("Profile %s: no pics found." % name)
# Iterate over pictures and download them
self._log("Retrieving posts from profile {}.".format(name))
totalcount = profile_metadata["user"]["media"]["count"]
if "media" in profile_metadata["user"]:
# backwards compatibility with old non-graphql structure
totalcount = profile_metadata["user"]["media"]["count"]
else:
totalcount = profile_metadata["user"]["edge_owner_to_timeline_media"]["count"]
count = 1
for post in self.get_profile_posts(profile_metadata):
self._log("[%3i/%3i] " % (count, totalcount), end="", flush=True)
@@ -1320,7 +1508,8 @@ class Instaloader:
def main(self, profilelist: List[str], username: Optional[str] = None, password: Optional[str] = None,
sessionfile: Optional[str] = None, max_count: Optional[int] = None,
profile_pic_only: bool = False, fast_update: bool = False,
profile_pic: bool = True, profile_pic_only: bool = False,
fast_update: bool = False,
stories: bool = False, stories_only: bool = False,
filter_str: Optional[str] = None) -> None:
"""Download set of profiles, hashtags etc. and handle logging in and session files if desired."""
@@ -1338,7 +1527,7 @@ class Instaloader:
if sessionfile is not None:
print(err, file=sys.stderr)
self._log("Session file does not exist yet - Logging in.")
if not self.is_logged_in or username != self.test_login(self.session):
if not self.is_logged_in or username != self.test_login():
if password is not None:
self.login(username, password)
else:
@@ -1376,6 +1565,14 @@ class Instaloader:
self.download_stories(fast_update=fast_update)
else:
self.error("--login=USERNAME required to download {}.".format(pentry))
elif pentry == ":saved":
if username is not None:
self._log("Retrieving saved posts...")
with self._error_catcher():
self.download_saved_posts(fast_update=fast_update, max_count=max_count,
filter_func=filter_func)
else:
self.error("--login=USERNAME required to download {}.".format(pentry))
else:
targets.add(pentry)
if len(targets) > 1:
@@ -1384,7 +1581,7 @@ class Instaloader:
for target in targets:
with self._error_catcher():
try:
self.download_profile(target, profile_pic_only, fast_update, stories, stories_only,
self.download_profile(target, profile_pic, profile_pic_only, fast_update, stories, stories_only,
filter_func=filter_func)
except ProfileNotExistsException as err:
if username is not None:
@@ -1392,8 +1589,8 @@ class Instaloader:
self._log("Trying again anonymously, helps in case you are just blocked.")
with self.anonymous_copy() as anonymous_loader:
with self._error_catcher():
anonymous_loader.download_profile(target, profile_pic_only, fast_update,
filter_func=filter_func)
anonymous_loader.download_profile(target, profile_pic, profile_pic_only,
fast_update, filter_func=filter_func)
else:
raise err
except KeyboardInterrupt:
@@ -1401,6 +1598,14 @@ class Instaloader:
# Save session if it is useful
if username is not None:
self.save_session_to_file(sessionfile)
# User might be confused if Instaloader does nothing
if not profilelist:
if self.is_logged_in:
# Instaloader did at least save a session file
self._log("No targets were specified, thus nothing has been downloaded.")
else:
# Instloader did not do anything
self._log("usage:"+USAGE_STRING)
if self.error_log:
print("\nErrors occured:", file=sys.stderr)
for err in self.error_log:
@@ -1408,10 +1613,10 @@ class Instaloader:
def main():
parser = ArgumentParser(description=__doc__, add_help=False,
epilog="Report issues at https://github.com/Thammus/instaloader/issues. "
parser = ArgumentParser(description=__doc__, add_help=False, usage=USAGE_STRING,
epilog="Report issues at https://github.com/instaloader/instaloader/issues. "
"The complete documentation can be found at "
"https://instaloader.readthedocs.io/.")
"https://instaloader.github.io/.")
g_what = parser.add_argument_group('What to Download',
'Specify a list of profiles or #hashtags. For each of these, Instaloader '
@@ -1423,13 +1628,18 @@ def main():
g_what.add_argument('profile', nargs='*', metavar='profile|#hashtag',
help='Name of profile or #hashtag to download. '
'Alternatively, if --login is given: @<profile> to download all followees of '
'<profile>; the special targets :feed to '
'download pictures from your feed; or :stories to download the stories of your '
'followees.')
'<profile>; the special targets '
':feed to download pictures from your feed; '
':stories to download the stories of your followees; or '
':saved to download the posts marked as saved.')
g_what.add_argument('-P', '--profile-pic-only', action='store_true',
help='Only download profile picture.')
g_what.add_argument('--no-profile-pic', action='store_true',
help='Do not download profile picture.')
g_what.add_argument('-V', '--no-videos', action='store_true',
help='Do not download videos.')
g_what.add_argument('--no-video-thumbnails', action='store_true',
help='Do not download thumbnails of videos.')
g_what.add_argument('-G', '--geotags', action='store_true',
help='Download geotags when available. Geotags are stored as a '
'text file with the location\'s name and a Google Maps link. '
@@ -1450,7 +1660,7 @@ def main():
help='Also download stories of each profile that is downloaded. Requires --login.')
g_what.add_argument('--stories-only', action='store_true',
help='Rather than downloading regular posts of each specified profile, only download '
'stories. Requires --login.')
'stories. Requires --login. Does not imply --no-profile-pic.')
g_what.add_argument('--only-if', metavar='filter',
help='Expression that, if given, must evaluate to True for each post to be downloaded. Must be '
'a syntactically valid python expression. Variables are evaluated to '
@@ -1523,6 +1733,7 @@ def main():
"eventually --only-if=viewer_has_liked.")
download_videos = Tristate.always if not args.no_videos else Tristate.no_extra_query
download_video_thumbnails = Tristate.always if not args.no_video_thumbnails else Tristate.never
download_comments = Tristate.always if args.comments else Tristate.no_extra_query
save_captions = Tristate.no_extra_query if not args.no_captions else Tristate.never
save_metadata = Tristate.always if args.metadata_json else Tristate.never
@@ -1539,13 +1750,21 @@ def main():
loader = Instaloader(sleep=not args.no_sleep, quiet=args.quiet,
user_agent=args.user_agent,
dirname_pattern=args.dirname_pattern, filename_pattern=args.filename_pattern,
download_videos=download_videos, download_geotags=download_geotags,
download_videos=download_videos, download_video_thumbnails=download_video_thumbnails,
download_geotags=download_geotags,
save_captions=save_captions, download_comments=download_comments,
save_metadata=save_metadata, max_connection_attempts=args.max_connection_attempts)
loader.main(args.profile, args.login.lower() if args.login is not None else None, args.password,
args.sessionfile,
int(args.count) if args.count is not None else None,
args.profile_pic_only, args.fast_update, args.stories, args.stories_only, args.only_if)
loader.main(args.profile,
username=args.login.lower() if args.login is not None else None,
password=args.password,
sessionfile=args.sessionfile,
max_count=int(args.count) if args.count is not None else None,
profile_pic=not args.no_profile_pic,
profile_pic_only=args.profile_pic_only,
fast_update=args.fast_update,
stories=args.stories,
stories_only=args.stories_only,
filter_str=args.only_if)
except InstaloaderException as err:
raise SystemExit("Fatal error: %s" % err)
+1 -1
View File
@@ -38,7 +38,7 @@ setup(
name='instaloader',
version=get_version(),
py_modules=['instaloader'],
url='https://instaloader.readthedocs.io/',
url='https://instaloader.github.io/',
license='MIT',
author='Alexander Graf, André Koch-Kramer',
author_email='mail@agraf.me, koch-kramer@web.de',