Compare commits

...
42 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
24 changed files with 477 additions and 172 deletions
+18 -9
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,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
+6 -7
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,
@@ -24,9 +23,9 @@ Instaloader
instaloader [--comments] [--geotags] [--stories]
[--login YOUR-USERNAME] [--fast-update]
profile | "#hashtag" | :stories | :feed
profile | "#hashtag" | :stories | :feed | :saved
`Instaloader Documentation <https://instaloader.readthedocs.io/>`__
`Instaloader Documentation <https://instaloader.github.io/>`__
How to Automatically Download Pictures from Instagram
@@ -65,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
+1 -1
View File
@@ -3,7 +3,7 @@
This directory contains sources for the Instaloader documentation.
The documentation is hosted at:
https://instaloader.readthedocs.io/
https://instaloader.github.io/
## Build
+3
View File
@@ -0,0 +1,3 @@
code {
color: #222;
}
-2
View File
@@ -1,2 +0,0 @@
<h2><a href="index.html">Instaloader</a></h2>
<p>Download pictures (or videos) along with their captions and other metadata from Instagram.</p>
+8 -2
View File
@@ -1,5 +1,11 @@
{% extends "!layout.html" %}
{% block extrahead %}
<meta name="google-site-verification" content="0esgS4FkYROvPe-I9MYvC1tTYtfASPpqxBdpTHEBIcQ" />
<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 %}
-23
View File
@@ -1,23 +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>Current Release</h3>
<ul>
<li class="toctree-l1"><a href="https://github.com/Thammus/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://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>
+5 -4
View File
@@ -1,7 +1,7 @@
.. meta::
:description:
Instaloader can also be used as a powerful and easy-to-use
Python API for Instagram, allowing to download media and metadata.
Documentation of Instaloader module, a powerful and easy-to-use
Python library to download Instagram media and metadata.
Python Module :mod:`instaloader`
--------------------------------
@@ -37,7 +37,8 @@ certain source::
L.download_post(post, target='#cat')
Besides :func:`Instaloader.get_hashtag_posts`, there is
:func:`Instaloader.get_feed_posts` and :func:`Instaloader.get_profile_posts`.
: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`.
@@ -68,7 +69,7 @@ call::
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/Thammus/instaloader>`__.
`Instaloader on Github <https://github.com/instaloader/instaloader>`__.
``Instaloader`` (Main Class)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+10
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`).
+7 -6
View File
@@ -7,7 +7,8 @@ Instaloader is invoked with::
where ``target`` is a ``profile``, a ``"#hashtag"``, ``@profile`` (all profiles
that *profile* is following), or if logged in ``:feed`` (pictures from your
feed) or ``:stories`` (stories of your followees).
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
@@ -16,11 +17,11 @@ their descriptions, you may also run ``instaloader --help``.
What to Download
^^^^^^^^^^^^^^^^
Specify a list of targets (profiles, #hashtags, ``:feed`` or ``:stories``). 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.
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
+21 -18
View File
@@ -18,10 +18,11 @@
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os
import subprocess
import sys
import re
import requests
from datetime import datetime
import sphinx_bootstrap_theme
sys.path.insert(0, os.path.abspath('..'))
# -- General configuration ------------------------------------------------
@@ -135,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 = []
@@ -161,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,
@@ -193,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.
@@ -357,13 +363,10 @@ texinfo_documents = [
# texinfo_no_detailmenu = False
def get_latest_tag(repo):
tags = requests.get("https://api.github.com/repos/{}/git/refs/tags".format(repo)).json()
latest_tag = requests.get(tags[-1]['object']['url']).json()
version_string = latest_tag['tag']
version_date = datetime.strptime(latest_tag['tagger']['date'], "%Y-%m-%dT%H:%M:%SZ")
return version_string[1:], re.sub(r'\b0+(\d)', r'\1', "{:%d %b %Y}".format(version_date))
current_release, current_release_date = get_latest_tag('Thammus/instaloader')
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")
+4 -4
View File
@@ -6,14 +6,14 @@ 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/Thammus/instaloader>`__, where Issues and Pull
`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/Thammus/instaloader/issues>`__. Since
`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:
@@ -40,7 +40,7 @@ Writing Code or Improving the Documentation
-------------------------------------------
Changes of the Instaloader source can be proposed as a
`Pull Request <https://github.com/Thammus/instaloader/pulls>`__. There are only
`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
@@ -64,7 +64,7 @@ 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/Thammus/instaloader/issues>`__.
`Issue Tracker <https://github.com/instaloader/instaloader/issues>`__.
Please consider the following:
- Instaloader already has plenty of features. **Check the documentation**
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

+12 -10
View File
@@ -1,8 +1,9 @@
.. meta::
:description:
Command line tool to download pictures (and videos) from Instagram.
Instaloader downloads public and private profiles, hashtags, user stories,
feeds, comments, geotags, captions and other metadata of each post.
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
===========
@@ -20,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,
@@ -31,14 +32,15 @@ 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
profile | "#hashtag" |
:stories | :feed | :saved
Table of Contents
@@ -56,10 +58,10 @@ Table of Contents
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
+6 -3
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)
@@ -28,7 +28,7 @@ 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 to **install Instaloader manually**,
`Download the Source <https://github.com/Thammus/instaloader/releases>`__,
`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
@@ -36,3 +36,6 @@ will be installed automatically, if it is not already installed.
- 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

+1
View File
@@ -1,3 +1,4 @@
requests
sphinx
sphinx-autodoc-typehints
sphinx-bootstrap-theme
+191 -80
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.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
@@ -127,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
@@ -169,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.
@@ -364,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
@@ -376,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]:
@@ -418,7 +447,17 @@ 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,
@@ -438,18 +477,19 @@ 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.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 = re.sub(r"(?i)({(?:post\.)?date_utc:[^}]*?)_UTC",
r"\1", self.filename_pattern_old)
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 = filename_pattern.replace('{date_utc}', '{date_utc:%Y-%m-%d_%H-%M-%S_UTC}')
self.filename_pattern = _PathPattern(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.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
@@ -548,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.
@@ -570,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)
@@ -585,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:
@@ -601,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)
@@ -617,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)
@@ -651,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.
@@ -670,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.")
@@ -698,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'] = 200
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
@@ -858,8 +901,14 @@ class Instaloader:
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"]
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')
@@ -877,13 +926,7 @@ class Instaloader:
if os.path.isfile(filename):
self._log(filename + ' already exists')
return None
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)
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
@@ -918,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"""
@@ -939,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!')
@@ -1158,30 +1202,22 @@ class Instaloader:
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:
@@ -1214,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
@@ -1306,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))
@@ -1314,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': 200,
'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']
@@ -1378,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)
@@ -1433,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:
@@ -1471,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:
@@ -1496,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:
@@ -1503,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 '
@@ -1518,9 +1628,10 @@ 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',
+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',