mirror of
https://github.com/instaloader/instaloader.git
synced 2026-10-16 21:52:17 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7f89e4327 | ||
|
|
af1ec22d41 | ||
|
|
f55334cd5b | ||
|
|
b2f31e7782 | ||
|
|
68d8f65cb9 | ||
|
|
e97e4ecdc8 | ||
|
|
e77c998a94 | ||
|
|
861d7b92f1 | ||
|
|
bfc882306b | ||
|
|
14e9cf435e | ||
|
|
83b31175df | ||
|
|
92114f9018 | ||
|
|
58a80c349d | ||
|
|
5f34fca9e7 | ||
|
|
fd6389249c | ||
|
|
374f13adbf | ||
|
|
17e443a7e2 | ||
|
|
4628f1cc8d | ||
|
|
e97411a502 | ||
|
|
304cde64fe | ||
|
|
3dbce922c3 | ||
|
|
4e57ac77f0 | ||
|
|
de960b2b57 | ||
|
|
9b60adeb45 | ||
|
|
0333bb5e99 | ||
|
|
b1edaddb31 | ||
|
|
01c2a2b1cb | ||
|
|
541b29b2e9 | ||
|
|
6f3afd2fa1 | ||
|
|
882d460a67 | ||
|
|
9b11ca18d1 | ||
|
|
39bd7c55f6 | ||
|
|
3654d6bf6d | ||
|
|
1ca368fa45 | ||
|
|
698ea51cb9 | ||
|
|
e6be0179b7 | ||
|
|
97e1bd2bf0 | ||
|
|
5695bca9f3 | ||
|
|
75d9dda4ba | ||
|
|
41a50ffa5a | ||
|
|
3daaa6fbbc | ||
|
|
341030058b | ||
|
|
fd98e8e78f | ||
|
|
e24c6a2cad | ||
|
|
01e549275e | ||
|
|
37780986fc | ||
|
|
9f34e3303e | ||
|
|
d5a8d96bd5 | ||
|
|
e9207f095f | ||
|
|
ce13c0c53c | ||
|
|
7d5bf0e8df | ||
|
|
bdf31864eb | ||
|
|
32388b884f |
+19
-10
@@ -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,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
@@ -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
@@ -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
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# Instaloader Documentation
|
||||
|
||||
This directory contains sources for the Instaloader documentation.
|
||||
|
||||
The documentation is hosted at:
|
||||
https://instaloader.github.io/
|
||||
|
||||
## Build
|
||||
|
||||
The documentation is created with [Sphinx](http://www.sphinx-doc.org/). To build it, use
|
||||
|
||||
```
|
||||
pip3 install -r requirements.txt
|
||||
make html
|
||||
```
|
||||
Vendored
+2
-1
@@ -1 +1,2 @@
|
||||
<h2>Instaloader</h2>
|
||||
<h2><a href="index.html">Instaloader</a></h2>
|
||||
<p>Download pictures (or videos) along with their captions and other metadata from Instagram.</p>
|
||||
|
||||
Vendored
+1
@@ -1,4 +1,5 @@
|
||||
{% extends "!layout.html" %}
|
||||
{% block extrahead %}
|
||||
<meta name="google-site-verification" content="FNUsDdkOJM9obm9QEdrNBVd9AtAzHzA-aaSMCUrS6C8" />
|
||||
<script async defer src="https://buttons.github.io/buttons.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
Vendored
+9
-4
@@ -4,15 +4,20 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="{{ next.link|e }}">{{ next.title }}</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<h3>Current Release</h3>
|
||||
<ul>
|
||||
<li class="toctree-l1"><a href="https://github.com/instaloader/instaloader/releases/tag/v{{current_release}}">Version <b>{{current_release}}</b></a></li>
|
||||
<li>Released on <b>{{current_release_date}}</b>.</li>
|
||||
</ul>
|
||||
<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://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/Thammus/instaloader">Instaloader on GitHub</a></li>
|
||||
<li><a href="https://github.com/instaloader/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>
|
||||
<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/Instaloader on GitHub">Star</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
Vendored
+10
@@ -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 %}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
<h3>New Documentation URL</h3>
|
||||
<div class="danger"><p style="margin-left: 1em"><b>Instaloader's Documentation has been moved to
|
||||
<a class="reference external" href="https://instaloader.github.io/">https://instaloader.github.io</a>.</b></p></div>
|
||||
+46
-43
@@ -1,3 +1,8 @@
|
||||
.. meta::
|
||||
:description:
|
||||
Instaloader can also be used as a powerful and easy-to-use
|
||||
Python API for Instagram, allowing to download 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)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -63,6 +63,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,8 +98,9 @@ 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
|
||||
defined.
|
||||
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}``.
|
||||
|
||||
For example, encode the poster's profile name in the filenames with:
|
||||
|
||||
|
||||
+34
-19
@@ -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
|
||||
|
||||
@@ -92,7 +104,7 @@ matching the specified targets.
|
||||
.. option:: --count COUNT
|
||||
|
||||
Do not attempt to download more than COUNT posts. Applies only to
|
||||
``#hashtag``, ``:feed-all`` and ``:feed-liked``.
|
||||
``#hashtag`` and ``:feed``.
|
||||
|
||||
|
||||
Login (Download Private Profiles)
|
||||
@@ -133,9 +145,12 @@ 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
|
||||
``{shortcode}`` can be specified. Defaults to ``{date:%Y-%m-%d_%H-%M-%S}``.
|
||||
See :ref:`filename-specification`.
|
||||
``#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}``.
|
||||
Defaults to
|
||||
``{date:%Y-%m-%d_%H-%M-%S}``. See :ref:`filename-specification`.
|
||||
|
||||
.. option:: --user-agent USER_AGENT
|
||||
|
||||
@@ -145,7 +160,7 @@ How to Download
|
||||
.. option:: --max-connection-attempts N
|
||||
|
||||
Maximum number of connection attempts until a request is aborted. Defaults
|
||||
to ``3``. If a connection fails, it can by manually skipped by hitting
|
||||
to ``3``. If a connection fails, it can be manually skipped by hitting
|
||||
:kbd:`Control-c`. Set this to ``0`` to retry infinitely.
|
||||
|
||||
Miscellaneous Options
|
||||
|
||||
+13
-3
@@ -18,6 +18,7 @@
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath('..'))
|
||||
|
||||
@@ -158,13 +159,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 +191,10 @@ html_static_path = ['_static']
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#
|
||||
html_sidebars = {'**': ["caption.html", "globaltoc.html", "relations.html", "links.html"] }
|
||||
if not os.environ.get("READTHEDOCS"):
|
||||
html_sidebars = {'**': ["caption.html", "globaltoc.html", "relations.html", "links.html"] }
|
||||
else:
|
||||
html_sidebars = {'**': ["caption.html", "rtdmessage.html", "globaltoc.html", "relations.html", "links.html"] }
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
@@ -352,3 +356,9 @@ 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}
|
||||
|
||||
@@ -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.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 330 KiB |
+22
-6
@@ -1,6 +1,14 @@
|
||||
.. meta::
|
||||
:description:
|
||||
Command line tool to download pictures (and videos) from Instagram.
|
||||
Instaloader downloads public and private profiles, hashtags, user stories,
|
||||
feeds, saved media, comments, geotags, captions and other metadata of each post.
|
||||
|
||||
Instaloader
|
||||
===========
|
||||
|
||||
.. highlight:: none
|
||||
|
||||
**Instaloader** is a tool to download pictures (or videos) along with
|
||||
their captions and other metadata from Instagram.
|
||||
|
||||
@@ -12,8 +20,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 +31,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 +52,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
|
||||
|
||||
+14
-6
@@ -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,15 @@ 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>`__,
|
||||
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>`__.
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
+117
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 |
@@ -1 +1,3 @@
|
||||
requests
|
||||
sphinx
|
||||
sphinx-autodoc-typehints
|
||||
|
||||
+366
-149
@@ -22,14 +22,14 @@ 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.1'
|
||||
__version__ = '3.3.2'
|
||||
|
||||
|
||||
try:
|
||||
@@ -53,6 +53,10 @@ class QueryReturnedNotFoundException(InstaloaderException):
|
||||
pass
|
||||
|
||||
|
||||
class QueryReturnedForbiddenException(InstaloaderException):
|
||||
pass
|
||||
|
||||
|
||||
class ProfileNotExistsException(InstaloaderException):
|
||||
pass
|
||||
|
||||
@@ -126,7 +130,7 @@ def mediaid_to_shortcode(mediaid: int) -> str:
|
||||
def format_string_contains_key(format_string: str, 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:
|
||||
if field_name == key or field_name.startswith(key + '.'):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -165,7 +169,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.
|
||||
@@ -176,7 +181,8 @@ class Post:
|
||||
|
||||
LOGIN_REQUIRING_PROPERTIES = ["viewer_has_liked"]
|
||||
|
||||
def __init__(self, instaloader: 'Instaloader', node: Dict[str, Any], profile: Optional[str] = None):
|
||||
def __init__(self, instaloader: 'Instaloader', node: Dict[str, Any],
|
||||
profile: Optional[str] = None, profile_id: Optional[int] = None):
|
||||
"""Create a Post instance from a node structure as returned by Instagram.
|
||||
|
||||
:param instaloader: :class:`Instaloader` instance used for additional queries if neccessary.
|
||||
@@ -186,6 +192,7 @@ class Post:
|
||||
self._instaloader = instaloader
|
||||
self._node = node
|
||||
self._profile = profile
|
||||
self._profile_id = profile_id
|
||||
self._full_metadata_dict = None
|
||||
|
||||
@classmethod
|
||||
@@ -206,6 +213,11 @@ class Post:
|
||||
"""Media shortcode. URL of the post is instagram.com/p/<shortcode>/."""
|
||||
return self._node['shortcode'] if 'shortcode' in self._node else self._node['code']
|
||||
|
||||
@property
|
||||
def mediaid(self) -> int:
|
||||
"""The mediaid is a decimal representation of the media shortcode."""
|
||||
return int(self._node['id'])
|
||||
|
||||
def __repr__(self):
|
||||
return '<Post {}>'.format(self.shortcode)
|
||||
|
||||
@@ -253,10 +265,22 @@ class Post:
|
||||
return 'UNKNOWN'
|
||||
|
||||
@property
|
||||
def date(self) -> datetime:
|
||||
"""Timestamp when the post was created."""
|
||||
def owner_id(self) -> int:
|
||||
"""The ID of the Post's owner."""
|
||||
if self._profile_id:
|
||||
return self._profile_id
|
||||
return int(self._field('owner', 'id'))
|
||||
|
||||
@property
|
||||
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"""
|
||||
@@ -341,7 +365,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
|
||||
@@ -353,6 +381,23 @@ 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")
|
||||
@@ -396,12 +441,15 @@ class Tristate(Enum):
|
||||
|
||||
|
||||
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,
|
||||
@@ -415,9 +463,19 @@ class Instaloader:
|
||||
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}'
|
||||
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 = 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 = filename_pattern.replace('{date_utc}', '{date_utc:%Y-%m-%d_%H-%M-%S_UTC}')
|
||||
else:
|
||||
self.filename_pattern = '{date_utc:%Y-%m-%d_%H-%M-%S_UTC}'
|
||||
self.filename_pattern_old = '{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
|
||||
@@ -440,7 +498,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
|
||||
@@ -493,6 +553,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))
|
||||
@@ -612,12 +675,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.
|
||||
@@ -631,8 +694,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.")
|
||||
@@ -659,17 +723,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
|
||||
|
||||
@@ -698,17 +763,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-z]*\\?', url)
|
||||
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
|
||||
@@ -719,15 +791,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]]
|
||||
@@ -740,17 +817,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)
|
||||
@@ -758,15 +843,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:
|
||||
@@ -789,8 +881,11 @@ 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."""
|
||||
|
||||
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')
|
||||
@@ -808,8 +903,13 @@ 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)
|
||||
url_best = re.sub(r'/s([1-9][0-9]{2})x\1/', '/s2048x2048/', url)
|
||||
url_best = re.sub(r'/vp/[a-f0-9]{32}/[A-F0-9]{8}/', '/', url_best) # remove signature
|
||||
try:
|
||||
self._get_and_write_raw(url_best, filename)
|
||||
except (QueryReturnedForbiddenException, QueryReturnedNotFoundException) as err:
|
||||
self.error('{} Retrying with lower quality version.'.format(err))
|
||||
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
|
||||
|
||||
@@ -844,11 +944,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"""
|
||||
@@ -865,10 +964,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!')
|
||||
@@ -889,52 +990,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.
|
||||
@@ -993,6 +1110,10 @@ class Instaloader:
|
||||
:param filename_target: Replacement for {target} in dirname_pattern and filename_pattern
|
||||
"""
|
||||
|
||||
if format_string_contains_key(self.filename_pattern, 'post'):
|
||||
raise InvalidArgumentException("The \"post\" keyword is not supported in the filename pattern when "
|
||||
"downloading stories.")
|
||||
|
||||
if not self.is_logged_in:
|
||||
raise LoginRequiredException('Login required to download stories')
|
||||
|
||||
@@ -1006,71 +1127,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:
|
||||
@@ -1103,6 +1233,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
|
||||
@@ -1195,30 +1375,42 @@ 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))
|
||||
|
||||
def get_profile_posts(self, profile_metadata: Dict[str, Any]) -> Iterator[Post]:
|
||||
"""Retrieve all posts from a profile."""
|
||||
profile_name = profile_metadata['user']['username']
|
||||
yield from (Post(self, node, profile=profile_name) 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']
|
||||
profile_id = int(profile_metadata['user']['id'])
|
||||
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']
|
||||
yield from (Post(self, edge['node'], profile=profile_name) for edge in media['edges'])
|
||||
yield from (Post(self, edge['node'], profile=profile_name, profile_id=profile_id)
|
||||
for edge in media['edges'])
|
||||
has_next_page = media['page_info']['has_next_page']
|
||||
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"""
|
||||
@@ -1239,8 +1431,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
|
||||
|
||||
@@ -1262,13 +1455,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)
|
||||
@@ -1298,7 +1491,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."""
|
||||
@@ -1316,7 +1510,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:
|
||||
@@ -1354,6 +1548,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:
|
||||
@@ -1362,7 +1564,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:
|
||||
@@ -1370,8 +1572,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:
|
||||
@@ -1387,9 +1589,9 @@ class Instaloader:
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(description=__doc__, add_help=False,
|
||||
epilog="Report issues at https://github.com/Thammus/instaloader/issues. "
|
||||
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 '
|
||||
@@ -1401,13 +1603,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. '
|
||||
@@ -1428,7 +1635,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 '
|
||||
@@ -1442,7 +1649,7 @@ def main():
|
||||
'flag is recommended when you use Instaloader to update your personal Instagram archive.')
|
||||
g_stop.add_argument('-c', '--count',
|
||||
help='Do not attempt to download more than COUNT posts. '
|
||||
'Applies only to #hashtag, :feed-all and :feed-liked.')
|
||||
'Applies only to #hashtag and :feed.')
|
||||
|
||||
g_login = parser.add_argument_group('Login (Download Private Profiles)',
|
||||
'Instaloader can login to Instagram. This allows downloading private profiles. '
|
||||
@@ -1468,14 +1675,15 @@ def main():
|
||||
help='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 shortcode can be specified. Defaults to '
|
||||
'\'{date:%%Y-%%m-%%d_%%H-%%M-%%S}\'.')
|
||||
'profile name. Also, the fields {date} and {shortcode} can be specified. In case of not '
|
||||
'downloading stories, the attributes of the Post class can be used in addition, e.g. '
|
||||
'{post.owner_id} or {post.mediaid}. Defaults to \'{date:%%Y-%%m-%%d_%%H-%%M-%%S}\'.')
|
||||
g_how.add_argument('--user-agent',
|
||||
help='User Agent to use for HTTP requests. Defaults to \'{}\'.'.format(default_user_agent()))
|
||||
g_how.add_argument('-S', '--no-sleep', action='store_true', help=SUPPRESS)
|
||||
g_how.add_argument('--max-connection-attempts', metavar='N', type=int, default=3,
|
||||
help='Maximum number of connection attempts until a request is aborted. Defaults to 3. If a '
|
||||
'connection fails, it can by manually skipped by hitting CTRL+C. Set this to 0 to retry '
|
||||
'connection fails, it can be manually skipped by hitting CTRL+C. Set this to 0 to retry '
|
||||
'infinitely.')
|
||||
|
||||
g_misc = parser.add_argument_group('Miscellaneous Options')
|
||||
@@ -1500,6 +1708,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
|
||||
@@ -1516,13 +1725,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)
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user