Compare commits

...
29 Commits
Author SHA1 Message Date
Alexander Graf 14f1c3cb82 Release of version 4.11 2024-03-18 08:08:09 +01:00
Alexander Graf 7a36512093 Third release candidate of version 4.11 2024-03-17 21:08:38 +01:00
Alexander Graf f423aa1507 Update dependencies 2024-03-17 21:04:56 +01:00
Alexander Graf 1875aede82 Merge branch 'master' into upcoming/v4.11 2024-03-17 20:49:37 +01:00
Alexander Graf 2cae933d66 Fix downloading comments
Closes issue #2125 by using an iphone endpoint for posts that have more
than 50 comments.
2024-03-17 20:42:26 +01:00
Alexander Graf 5509ea7daf Update sponsors in README 2024-01-12 08:29:09 +01:00
Alexander Graf 2b594909bb Release of version 4.10.3 2024-01-12 08:15:56 +01:00
Alexander Graf 1852447825 Fix downloading comments
Closes issue #2125 by using an iphone endpoint for posts that have more
than 50 comments.
2024-01-12 08:13:45 +01:00
Eduardo Kalinowski baaa2c2998 Add myself to AUTHORS.md (#2160) 2024-01-08 12:15:13 +01:00
Eduardo Kalinowski 47235bf2b8 Change pinned post detection, fixes #2142 (#2147)
The property that indicated whether a post is pinned is not available
anymore. As a workaround, the first five posts are treated differently
(as pinned posts were): if they were already downloaded, they are
skipped, but they do not cause the iteration to stop.
2024-01-08 07:46:27 +01:00
Alexander Graf 6eea8e6fcd Second release candidate of Version 4.11 2023-12-18 08:08:43 +01:00
Alexander Graf 5166fe691a Merge branch 'master' into upcoming/v4.11 2023-12-18 08:07:43 +01:00
Alexander Graf 9fa01509df Update pipfile.lock dependencies 2023-12-18 08:05:44 +01:00
Alexander Graf f0420e8a20 Update default user agent 2023-12-18 08:01:06 +01:00
Alexander Graf 7cc13ee08e Add versionadded directives and spacings 2023-12-18 07:58:53 +01:00
Juned Khan bf396f0e6c Implemented fix to avoid simuntaneous use of --login and --load-cookies (#2119) 2023-12-18 07:50:45 +01:00
Alexander Graf 39566735f7 Release of version 4.10.2 2023-12-09 17:59:08 +01:00
Alexander Graf fff25d6f4a Remove unnecessary do_sleep() 2023-12-09 17:58:40 +01:00
LostXOR b52547ea1c Fix 403 during login due to incorrect login endpoint and CSRF token location (#2133) 2023-12-09 17:56:21 +01:00
Alexander Graf 08bf0fef29 Attempt to fix publish package workflow 2023-11-15 08:02:57 +01:00
Alexander Graf 5efa5b3637 First release candidate of Version 4.11 2023-11-15 07:54:59 +01:00
Alexander Graf 82b46605a2 Add notice of 2FA currently not working 2023-11-15 07:53:45 +01:00
Alexander Graf f80eebec7a Update default user agent 2023-11-15 07:47:34 +01:00
Juned Khan 5a92f264c8 Load cookies from browser (#2040) 2023-11-15 07:32:18 +01:00
Alexander Graf acb65b5cca Merge branch 'master' into upcoming/v4.11 2023-10-16 08:54:59 +02:00
Alexander Graf 51a16a433e Release of version 4.10.1 2023-10-16 08:27:46 +02:00
Alexander Graf 6145c5b383 Temporarily disable lint check on python 3.11
It fails due to a problem with installing dependencies, which is already
fixed in a3dc75c04e.
2023-10-16 08:26:10 +02:00
Alexander Graf a3dc75c04e Update all dependencies 2023-10-14 17:31:41 +02:00
ybenel 3ebfe40729 Get comment likes (#1998) 2023-07-17 08:09:20 +02:00
22 changed files with 920 additions and 1218 deletions
+2 -2
View File
@@ -14,10 +14,10 @@ jobs:
- name: "Setup Python" - name: "Setup Python"
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.8 python-version: 3.12
- name: "Install Dependencies" - name: "Install Dependencies"
run: | run: |
python -m pip install pipenv==2023.2.4 python -m pip install pipenv==2023.12.1
pipenv --python `python --version | grep -Eo '3\.[0-9]+'` sync --dev pipenv --python `python --version | grep -Eo '3\.[0-9]+'` sync --dev
- name: "Build Documentation" - name: "Build Documentation"
run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run make -C docs html SPHINXOPTS="-W -n" run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run make -C docs html SPHINXOPTS="-W -n"
+2 -2
View File
@@ -9,7 +9,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"] python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
steps: steps:
- name: Checkout Instaloader Repository - name: Checkout Instaloader Repository
uses: actions/checkout@v2 uses: actions/checkout@v2
@@ -19,7 +19,7 @@ jobs:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install Dependencies - name: Install Dependencies
run: | run: |
python -m pip install pipenv==2023.2.4 python -m pip install pipenv==2023.12.1
pipenv --python `python --version | grep -Eo '3\.[0-9]+'` sync --dev pipenv --python `python --version | grep -Eo '3\.[0-9]+'` sync --dev
- name: PyLint - name: PyLint
run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run pylint instaloader run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run pylint instaloader
+3 -1
View File
@@ -15,13 +15,15 @@ jobs:
- name: "Setup Python" - name: "Setup Python"
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.8 python-version: 3.12
- name: "Get the tagged version" - name: "Get the tagged version"
id: get_version id: get_version
env: env:
GITHUB_REF: ${{ github.ref }} GITHUB_REF: ${{ github.ref }}
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/} run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/}
shell: bash shell: bash
- name: "Install Dependencies"
run: python -m pip install setuptools
- name: "Generate Python Package" - name: "Generate Python Package"
run: python setup.py sdist run: python setup.py sdist
- name: "Publish Distribution to PyPI" - name: "Publish Distribution to PyPI"
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: "3.9" python-version: "3.12"
architecture: x64 architecture: x64
- name: Get the tagged version - name: Get the tagged version
id: get_version id: get_version
+1
View File
@@ -6,4 +6,5 @@ Instaloader is written by
- Alexander Graf (@aandergr) - Alexander Graf (@aandergr)
- André Koch-Kramer (@Thammus) - André Koch-Kramer (@Thammus)
- Lars Lindqvist (@e5150) - Lars Lindqvist (@e5150)
- Eduardo M Kalinowksi (@ekalin)
- ... and many more, see https://github.com/instaloader/instaloader/graphs/contributors - ... and many more, see https://github.com/instaloader/instaloader/graphs/contributors
+8 -9
View File
@@ -6,18 +6,17 @@ verify_ssl = true
[dev-packages] [dev-packages]
pylint = "*" pylint = "*"
mypy = "*" mypy = "*"
sphinx = "~=2.3" sphinx = "*"
pyinstaller = "*" pyinstaller = "*"
pefile = "*"
pywin32-ctypes = "*"
psutil = "*" psutil = "*"
typing-extensions = "*"
types-requests = "*" types-requests = "*"
typed-ast = "*" sphinx-autodoc-typehints = "*"
jinja2 = "~=3.0.1" sphinxcontrib-jquery = "*"
wrapt = "*" # Needed for Python<3.11 tomli = "*" # Needed for Pylint for "python_version < '3.11'"
tomli = "*" # Needed for Python<3.11 dill = "*" # Needed for Pylint for "python_version < '3.11'"
dill = "*" # Needed for Python<3.11 pywin32-ctypes = "*" # Needed for pyinstaller on Windows
pefile = "*" # Needed for pyinstaller on Windows
[packages] [packages]
requests = "*" requests = "*"
browser-cookie3 = "*"
Generated
+542 -553
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -125,7 +125,6 @@ Supporters
| Instaloader is proudly sponsored by | Instaloader is proudly sponsored by
| `@rocketapi-io <https://github.com/rocketapi-io>`__ | `@rocketapi-io <https://github.com/rocketapi-io>`__
| `@socialmethod <https://github.com/socialmethod>`__
See `Alex' GitHub Sponsors <https://github.com/sponsors/aandergr>`__ page for See `Alex' GitHub Sponsors <https://github.com/sponsors/aandergr>`__ page for
how you can sponsor the development of Instaloader! how you can sponsor the development of Instaloader!
+1 -1
View File
@@ -44,7 +44,7 @@ with open('__main__.py', 'w+') as f:
f.writelines(lines) f.writelines(lines)
# install dependencies and invoke PyInstaller # install dependencies and invoke PyInstaller
commands = ["pip install pipenv==2023.2.4", commands = ["pip install pipenv==2023.12.1",
f"pipenv --python {sys.version_info.major}.{sys.version_info.minor} sync --dev", f"pipenv --python {sys.version_info.major}.{sys.version_info.minor} sync --dev",
"pipenv run pyinstaller --log-level=DEBUG instaloader.spec"] "pipenv run pyinstaller --log-level=DEBUG instaloader.spec"]
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -21,7 +21,7 @@
{% block header %} {% block header %}
<nav class="navbar navbar-light navbar-expand bg-light border-bottom"> <nav class="navbar navbar-light navbar-expand bg-light border-bottom">
<a class="navbar-brand" href="{{ pathto(master_doc) }}"> <a class="navbar-brand" href="{{ pathto(master_doc) }}">
<img src="{{ pathto('_static/' + logo, 1) }}" width="30" height="30" class="d-inline-block align-top" alt="logo"> <img src="{{ pathto('_static/logo.png', 1) }}" width="30" height="30" class="d-inline-block align-top" alt="logo">
<span class="ml-3">Instaloader</span> <span class="ml-3">Instaloader</span>
</a> </a>
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav mr-auto">
+20 -1
View File
@@ -196,6 +196,25 @@ Instaloader to login.
Login name (profile name) for your Instagram account. Login name (profile name) for your Instagram account.
.. option:: --load-cookies BROWSER-NAME, -b BROWSER-NAME
Use Instagram cookie in your browser to login.
This feature requires the browser_cookie3 library.
Compatible with :option:`--cookiefile` if you want to load cookies from browser profiles.
Incompatible with :option:`--login` due to potential username mismatch between user input and browser login.
Supported browsers: Chrome, Firefox, Edge, Opera, Safari, Brave.
After loading the cookies run the :option:`--login` option as it is required to download high quality media
and to make full use of Instaloader's features.
.. versionadded:: 4.11
.. option:: --cookiefile COOKIE-FILE, -B COOKIE-FILE
Cookie file path of a browser profile to load cookies from.
.. versionadded:: 4.11
.. option:: --sessionfile SESSIONFILE, -f SESSIONFILE .. option:: --sessionfile SESSIONFILE, -f SESSIONFILE
Path for loading and storing session key file. Defaults to Path for loading and storing session key file. Defaults to
@@ -281,7 +300,7 @@ How to Download
.. option:: --user-agent USER_AGENT .. option:: --user-agent USER_AGENT
User Agent to use for HTTP requests. Per default, Instaloader pretends being User Agent to use for HTTP requests. Per default, Instaloader pretends being
Chrome/92 on Linux. Chrome/120 on Linux.
.. option:: --max-connection-attempts N .. option:: --max-connection-attempts N
+63 -354
View File
@@ -1,56 +1,84 @@
#!/usr/bin/env python3 # Configuration file for the Sphinx documentation builder.
# -*- coding: utf-8 -*-
# #
# Instaloader documentation build configuration file, created by # For the full list of built-in configuration values, see the documentation:
# sphinx-quickstart on Fri Aug 25 15:38:26 2017. # https://www.sphinx-doc.org/en/master/usage/configuration.html
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
import os import os
import platform import platform
import subprocess import subprocess
import sys import sys
import typing
sys.path.insert(0, os.path.abspath('..')) sys.path.insert(0, os.path.abspath('..'))
import docs.sphinx_autodoc_typehints as sphinx_autodoc_typehints
# -- General configuration ------------------------------------------------ # -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
# If your documentation needs a minimal Sphinx version, state it here. project = 'Instaloader'
# copyright = '2023, Alexander Graf and Andre Koch-Kramer'
# needs_sphinx = '1.0' author = 'Alexander Graf and Andre Koch-Kramer'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [ extensions = [
'sphinx.ext.autodoc', "sphinx.ext.autodoc",
'sphinx.ext.githubpages', "sphinx.ext.extlinks",
'sphinx.ext.intersphinx', "sphinx.ext.githubpages",
'sphinx.ext.extlinks' "sphinx.ext.intersphinx",
"sphinxcontrib.jquery",
] ]
autodoc_default_flags = ['show-inheritance', 'members', 'undoc-members'] # autodoc options
autodoc_member_order = 'bysource' autodoc_default_options = {
'show-inheritance': True,
'members': True,
'undoc-members': True,
}
intersphinx_mapping = {'python': ('https://docs.python.org/3', None), # extlinks options
'requests': ('https://requests.readthedocs.io/en/latest/', None)}
nitpick_ignore = [('py:class', 'typing.Tuple')] extlinks = {
'issue': (
'https://github.com/instaloader/instaloader/issues/%s',
'Issue #%s'
),
'example': (
'https://raw.githubusercontent.com/instaloader/instaloader/master/docs/codesnippets/%s',
'Example %s'
),
}
# intersphinx options
intersphinx_mapping = {
'python': ('https://docs.python.org/3', None),
'requests': ('https://requests.readthedocs.io/en/latest/', None),
}
# other options
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# ignore TypeVar as it is currently unsupported by Sphinx
nitpick_ignore = [('py:class', 'instaloader.nodeiterator.T')]
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
pygments_style = 'fruity'
html_theme = 'basic'
html_static_path = ['_static']
html_logo = "logo.png"
html_css_files = ['instaloaderdoc.css']
html_js_files = ['instaloaderdoc.js']
# get release number
current_release = subprocess.check_output(["git", "describe", "--abbrev=0"]).decode("ascii")[1:-1] current_release = subprocess.check_output(["git", "describe", "--abbrev=0"]).decode("ascii")[1:-1]
date_format = "%e %b %Y" if platform.system() != "Windows" else "%d %b %Y" date_format = "%e %b %Y" if platform.system() != "Windows" else "%d %b %Y"
@@ -58,322 +86,3 @@ current_release_date = subprocess.check_output(
["git", "log", "-1", "--tags", "--format=%ad", "--date=format:"+date_format]).decode("ascii")[:-1] ["git", "log", "-1", "--tags", "--format=%ad", "--date=format:"+date_format]).decode("ascii")[:-1]
html_context = {'current_release': current_release, 'current_release_date': current_release_date} html_context = {'current_release': current_release, 'current_release_date': current_release_date}
extlinks = {
'issue': (
'https://github.com/instaloader/instaloader/issues/%s',
'Issue #'
),
'example': (
'https://raw.githubusercontent.com/instaloader/instaloader/master/docs/codesnippets/%s',
'Example '
),
}
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
#
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The encoding of source files.
#
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = 'Instaloader'
copyright = '2017-2020, Alexander Graf and Andre Koch-Kramer'
author = 'Alexander Graf and Andre Koch-Kramer'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
#version = '3.0'
# The full version, including alpha/beta/rc tags.
#release = '3.0rc0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = "en"
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#
# today = ''
#
# Else, today_fmt is used as the format for a strftime call.
#
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#
add_module_names = False
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'fruity'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
# keep_warnings = False
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'basic'
# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []
# The name for this set of Sphinx documents.
# "<project> v<release> documentation" by default.
#
# html_title = 'Instaloader v3.0rc0'
# A shorter title for the navigation bar. Default is the same as html_title.
#
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#
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 = "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,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#
# html_extra_path = []
# If not None, a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
# The empty string is equivalent to '%b %d, %Y'.
#
# html_last_updated_fmt = None
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#
#html_sidebars = {'**': ["relations.html", "links.html"] }
html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#
# html_additional_pages = {}
# If false, no module index is generated.
#
html_domain_indices = False
# If false, no index is generated.
#
html_use_index = True
# If true, the index is split into individual pages for each letter.
#
# html_split_index = False
# If true, links to the reST sources are added to the pages.
#
html_show_sourcelink = False
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#
html_show_sphinx = False
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#
html_show_copyright = False
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh'
#
# html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# 'ja' uses this config value.
# 'zh' user can custom change `jieba` dictionary path.
#
# html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'Instaloaderdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#
# 'preamble': '',
# Latex figure (float) alignment
#
# 'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'Instaloader.tex', 'Instaloader Documentation',
'Alexander Graf, André Koch-Kramer', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#
# latex_use_parts = False
# If true, show page references after internal links.
#
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
#
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
#
# latex_appendices = []
# It false, will not define \strong, \code, itleref, \crossref ... but only
# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added
# packages.
#
# latex_keep_old_macro_names = True
# If false, no module index is generated.
#
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'instaloader', 'Instaloader Documentation',
[author], 1)
]
# If true, show URL addresses after external links.
#
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'Instaloader', 'Instaloader Documentation',
author, 'Instaloader', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#
# texinfo_appendices = []
# If false, no module index is generated.
#
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#
# texinfo_no_detailmenu = False
def setup(app):
typing.TYPE_CHECKING = True
app.add_stylesheet("instaloaderdoc.css")
app.connect('autodoc-process-signature', sphinx_autodoc_typehints.process_signature)
app.connect('autodoc-process-docstring', sphinx_autodoc_typehints.process_docstring)
+8
View File
@@ -71,6 +71,14 @@ TopSearchResults
.. versionadded:: 4.3 .. versionadded:: 4.3
TitlePic
""""""""
.. autoclass:: TitlePic
:no-show-inheritance:
.. versionadded:: 4.8
Loading and Saving Loading and Saving
"""""""""""""""""" """"""""""""""""""
-251
View File
@@ -1,251 +0,0 @@
# sphinx_autodoc_typehints from Nov 20 2018 (version 1.5.2).
# https://github.com/agronholm/sphinx-autodoc-typehints
# (slightly modified to fix class links, maybe related to agronholm/sphinx-autodoc-typehints#38)
#
# This is the MIT license: http://www.opensource.org/licenses/mit-license.php
#
# Copyright (c) Alex Grönholm
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this
# software and associated documentation files (the "Software"), to deal in the Software
# without restriction, including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
# to whom the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
# FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
import inspect
from typing import get_type_hints, TypeVar, Any, AnyStr, Generic, Union
from sphinx.util import logging
from sphinx.util.inspect import Signature
try:
from inspect import unwrap
except ImportError:
def unwrap(func, *, stop=None):
"""This is the inspect.unwrap() method copied from Python 3.5's standard library."""
if stop is None:
def _is_wrapper(f):
return hasattr(f, '__wrapped__')
else:
def _is_wrapper(f):
return hasattr(f, '__wrapped__') and not stop(f)
f = func # remember the original func for error reporting
memo = {id(f)} # Memoise by id to tolerate non-hashable objects
while _is_wrapper(func):
func = func.__wrapped__
id_func = id(func)
if id_func in memo:
raise ValueError('wrapper loop when unwrapping {!r}'.format(f))
memo.add(id_func)
return func
logger = logging.getLogger(__name__)
def format_annotation(annotation):
if inspect.isclass(annotation) and annotation.__module__ == 'builtins':
if annotation.__qualname__ == 'NoneType':
return '``None``'
else:
return ':py:class:`{}`'.format(annotation.__qualname__)
annotation_cls = annotation if inspect.isclass(annotation) else type(annotation)
class_name = None
if annotation_cls.__module__ == 'typing':
params = None
prefix = ':py:class:'
module = 'typing'
extra = ''
if inspect.isclass(getattr(annotation, '__origin__', None)):
annotation_cls = annotation.__origin__
try:
if Generic in annotation_cls.mro():
module = annotation_cls.__module__
except TypeError:
pass # annotation_cls was either the "type" object or typing.Type
if annotation is Any:
return ':py:data:`~typing.Any`'
elif annotation is AnyStr:
return ':py:data:`~typing.AnyStr`'
elif isinstance(annotation, TypeVar):
return '\\%r' % annotation
elif (annotation is Union or getattr(annotation, '__origin__', None) is Union or
hasattr(annotation, '__union_params__')):
prefix = ':py:data:'
class_name = 'Union'
if hasattr(annotation, '__union_params__'):
params = annotation.__union_params__
elif hasattr(annotation, '__args__'):
params = annotation.__args__
if params and len(params) == 2 and (hasattr(params[1], '__qualname__') and
params[1].__qualname__ == 'NoneType'):
class_name = 'Optional'
params = (params[0],)
elif annotation_cls.__qualname__ == 'Tuple' and hasattr(annotation, '__tuple_params__'):
params = annotation.__tuple_params__
if annotation.__tuple_use_ellipsis__:
params += (Ellipsis,)
elif annotation_cls.__qualname__ == 'Callable':
prefix = ':py:data:'
arg_annotations = result_annotation = None
if hasattr(annotation, '__result__'):
arg_annotations = annotation.__args__
result_annotation = annotation.__result__
elif getattr(annotation, '__args__', None):
arg_annotations = annotation.__args__[:-1]
result_annotation = annotation.__args__[-1]
if arg_annotations in (Ellipsis, (Ellipsis,)):
params = [Ellipsis, result_annotation]
elif arg_annotations is not None:
params = [
'\\[{}]'.format(
', '.join(format_annotation(param) for param in arg_annotations)),
result_annotation
]
elif hasattr(annotation, 'type_var'):
# Type alias
class_name = annotation.name
params = (annotation.type_var,)
elif getattr(annotation, '__args__', None) is not None:
params = annotation.__args__
elif hasattr(annotation, '__parameters__'):
params = annotation.__parameters__
if params:
extra = '\\[{}]'.format(', '.join(format_annotation(param) for param in params))
if not class_name:
class_name = annotation_cls.__qualname__.title()
return '{}`~{}.{}`{}'.format(prefix, module, class_name, extra)
elif annotation is Ellipsis:
return '...'
elif inspect.isclass(annotation) or inspect.isclass(getattr(annotation, '__origin__', None)):
if not inspect.isclass(annotation):
annotation_cls = annotation.__origin__
extra = ''
if Generic in annotation_cls.mro():
params = (getattr(annotation, '__parameters__', None) or
getattr(annotation, '__args__', None))
extra = '\\[{}]'.format(', '.join(format_annotation(param) for param in params))
module = annotation.__module__.split('.')[0] # hack to 'fix' class linking for Instaloader project
return ':py:class:`~{}.{}`{}'.format(module, annotation_cls.__qualname__,
extra)
return str(annotation)
def process_signature(app, what: str, name: str, obj, options, signature, return_annotation):
if not callable(obj):
return
if what in ('class', 'exception'):
obj = getattr(obj, '__init__', getattr(obj, '__new__', None))
if not getattr(obj, '__annotations__', None):
return
obj = unwrap(obj)
signature = Signature(obj)
parameters = [
param.replace(annotation=inspect.Parameter.empty)
for param in signature.signature.parameters.values()
]
if parameters:
if what in ('class', 'exception'):
del parameters[0]
elif what == 'method':
outer = inspect.getmodule(obj)
for clsname in obj.__qualname__.split('.')[:-1]:
outer = getattr(outer, clsname)
method_name = obj.__name__
if method_name.startswith("__") and not method_name.endswith("__"):
# If the method starts with double underscore (dunder)
# Python applies mangling so we need to prepend the class name.
# This doesn't happen if it always ends with double underscore.
class_name = obj.__qualname__.split('.')[-2]
method_name = "_{c}{m}".format(c=class_name, m=method_name)
method_object = outer.__dict__[method_name]
if not isinstance(method_object, (classmethod, staticmethod)):
del parameters[0]
signature.signature = signature.signature.replace(
parameters=parameters,
return_annotation=inspect.Signature.empty)
return signature.format_args().replace('\\', '\\\\'), None
def process_docstring(app, what, name, obj, options, lines):
if isinstance(obj, property):
obj = obj.fget
if callable(obj):
if what in ('class', 'exception'):
obj = getattr(obj, '__init__')
obj = unwrap(obj)
try:
type_hints = get_type_hints(obj)
except (AttributeError, TypeError):
# Introspecting a slot wrapper will raise TypeError
return
except NameError as exc:
logger.warning('Cannot resolve forward reference in type annotations of "%s": %s',
name, exc)
type_hints = obj.__annotations__
for argname, annotation in type_hints.items():
if argname.endswith('_'):
argname = '{}\\_'.format(argname[:-1])
formatted_annotation = format_annotation(annotation)
if argname == 'return':
if what in ('class', 'exception'):
# Don't add return type None from __init__()
continue
insert_index = len(lines)
for i, line in enumerate(lines):
if line.startswith(':rtype:'):
insert_index = None
break
elif line.startswith(':return:') or line.startswith(':returns:'):
insert_index = i
if insert_index is not None:
if insert_index == len(lines):
# Ensure that :rtype: doesn't get joined with a paragraph of text, which
# prevents it being interpreted.
lines.append('')
insert_index += 1
lines.insert(insert_index, ':rtype: {}'.format(formatted_annotation))
else:
searchfor = ':param {}:'.format(argname)
for i, line in enumerate(lines):
if line.startswith(searchfor):
lines.insert(i, ':type {}: {}'.format(argname, formatted_annotation))
break
+3 -2
View File
@@ -1,7 +1,7 @@
"""Download pictures (or videos) along with their captions and other metadata from Instagram.""" """Download pictures (or videos) along with their captions and other metadata from Instagram."""
__version__ = '4.10' __version__ = '4.11'
try: try:
@@ -18,5 +18,6 @@ from .instaloadercontext import InstaloaderContext, RateController
from .lateststamps import LatestStamps from .lateststamps import LatestStamps
from .nodeiterator import NodeIterator, FrozenNodeIterator, resumable_iteration from .nodeiterator import NodeIterator, FrozenNodeIterator, resumable_iteration
from .structures import (Hashtag, Highlight, Post, PostSidecarNode, PostComment, PostCommentAnswer, PostLocation, from .structures import (Hashtag, Highlight, Post, PostSidecarNode, PostComment, PostCommentAnswer, PostLocation,
Profile, Story, StoryItem, TopSearchResults, load_structure_from_file, save_structure_to_file, Profile, Story, StoryItem, TopSearchResults, TitlePic,
load_structure_from_file, save_structure_to_file,
load_structure, get_json_structure) load_structure, get_json_structure)
+75 -2
View File
@@ -5,6 +5,7 @@ import datetime
import os import os
import re import re
import sys import sys
import textwrap
from argparse import ArgumentParser, ArgumentTypeError, SUPPRESS from argparse import ArgumentParser, ArgumentTypeError, SUPPRESS
from typing import List, Optional from typing import List, Optional
@@ -14,6 +15,11 @@ from . import (AbortDownloadException, BadCredentialsException, Instaloader, Ins
from .instaloader import (get_default_session_filename, get_default_stamps_filename) from .instaloader import (get_default_session_filename, get_default_stamps_filename)
from .instaloadercontext import default_user_agent from .instaloadercontext import default_user_agent
from .lateststamps import LatestStamps from .lateststamps import LatestStamps
try:
import browser_cookie3
bc3_library = True
except ImportError:
bc3_library = False
def usage_string(): def usage_string():
@@ -67,6 +73,53 @@ def filterstr_to_filterfunc(filter_str: str, item_type: type):
return filterfunc return filterfunc
def get_cookies_from_instagram(domain, browser, cookie_file='', cookie_name=''):
supported_browsers = {
"chrome": browser_cookie3.chrome,
"firefox": browser_cookie3.firefox,
"edge": browser_cookie3.edge,
"brave": browser_cookie3.brave,
"opera": browser_cookie3.opera,
"safari": browser_cookie3.safari
}
if browser not in supported_browsers:
print("Loading cookies from the specified browser failed")
print("Supported browsers are Chrome, Firefox, Edge, Brave, Opera and Safari")
return {}
cookies = {}
browser_cookies = list(supported_browsers[browser](cookie_file=cookie_file))
for cookie in browser_cookies:
if domain in cookie.domain:
cookies[cookie.name] = cookie.value
if cookies:
print(f"Cookies loaded successfully from {browser}")
else:
print(f"No cookies found for Instagram in {browser}, Are you logged in succesfully in {browser}?")
if cookie_name:
return cookies.get(cookie_name, {})
else:
return cookies
def import_session(browser, instaloader, cookiefile):
cookie = get_cookies_from_instagram('instagram', browser, cookiefile)
if cookie is not None:
instaloader.context.update_cookies(cookie)
username = instaloader.test_login()
if not username:
raise SystemExit(f"Not logged in. Are you logged in successfully in {browser}?")
instaloader.context.username = username
print(f"{username} has been successfully logged in.")
next_step_text = (f"Next: Run instaloader --login={username} as it is required to download high quality media "
"and to make full use of instaloader's features.")
print(textwrap.fill(next_step_text))
def _main(instaloader: Instaloader, targetlist: List[str], def _main(instaloader: Instaloader, targetlist: List[str],
username: Optional[str] = None, password: Optional[str] = None, username: Optional[str] = None, password: Optional[str] = None,
sessionfile: Optional[str] = None, sessionfile: Optional[str] = None,
@@ -78,7 +131,9 @@ def _main(instaloader: Instaloader, targetlist: List[str],
fast_update: bool = False, fast_update: bool = False,
latest_stamps_file: Optional[str] = None, latest_stamps_file: Optional[str] = None,
max_count: Optional[int] = None, post_filter_str: Optional[str] = None, max_count: Optional[int] = None, post_filter_str: Optional[str] = None,
storyitem_filter_str: Optional[str] = None) -> None: storyitem_filter_str: Optional[str] = None,
browser: Optional[str] = None,
cookiefile: Optional[str] = None) -> None:
"""Download set of profiles, hashtags etc. and handle logging in and session files if desired.""" """Download set of profiles, hashtags etc. and handle logging in and session files if desired."""
# Parse and generate filter function # Parse and generate filter function
post_filter = None post_filter = None
@@ -93,6 +148,11 @@ def _main(instaloader: Instaloader, targetlist: List[str],
if latest_stamps_file is not None: if latest_stamps_file is not None:
latest_stamps = LatestStamps(latest_stamps_file) latest_stamps = LatestStamps(latest_stamps_file)
instaloader.context.log(f"Using latest stamps from {latest_stamps_file}.") instaloader.context.log(f"Using latest stamps from {latest_stamps_file}.")
# load cookies if browser is not None
if browser and bc3_library:
import_session(browser.lower(), instaloader, cookiefile)
elif browser and not bc3_library:
raise SystemExit("browser_cookie3 library is needed to load cookies from browsers")
# Login, if desired # Login, if desired
if username is not None: if username is not None:
if not re.match(r"^[A-Za-z0-9._]+$", username): if not re.match(r"^[A-Za-z0-9._]+$", username):
@@ -108,6 +168,10 @@ def _main(instaloader: Instaloader, targetlist: List[str],
try: try:
instaloader.login(username, password) instaloader.login(username, password)
except TwoFactorAuthRequiredException: except TwoFactorAuthRequiredException:
# https://github.com/instaloader/instaloader/issues/1217
instaloader.context.error("Warning: There have been reports of 2FA currently not working. "
"Consider importing session cookies from your browser with "
"--load-cookies.")
while True: while True:
try: try:
code = input("Enter 2FA verification code: ") code = input("Enter 2FA verification code: ")
@@ -356,6 +420,10 @@ def main():
'to login.') 'to login.')
g_login.add_argument('-l', '--login', metavar='YOUR-USERNAME', g_login.add_argument('-l', '--login', metavar='YOUR-USERNAME',
help='Login name (profile name) for your Instagram account.') help='Login name (profile name) for your Instagram account.')
g_login.add_argument('-b', '--load-cookies', metavar='BROWSER-NAME',
help='Browser name to load cookies from Instagram')
g_login.add_argument('-B', '--cookiefile', metavar='COOKIE-FILE',
help='Cookie file of a profile to load cookies')
g_login.add_argument('-f', '--sessionfile', g_login.add_argument('-f', '--sessionfile',
help='Path for loading and storing session key file. ' help='Path for loading and storing session key file. '
'Defaults to ' + get_default_session_filename("<login_name>")) 'Defaults to ' + get_default_session_filename("<login_name>"))
@@ -444,6 +512,9 @@ def main():
if args.no_pictures and args.fast_update: if args.no_pictures and args.fast_update:
raise SystemExit('--no-pictures and --fast-update cannot be used together.') raise SystemExit('--no-pictures and --fast-update cannot be used together.')
if args.login and args.load_cookies:
raise SystemExit('--load-cookies and --login cannot be used together.')
# Determine what to download # Determine what to download
download_profile_pic = not args.no_profile_pic or args.profile_pic_only download_profile_pic = not args.no_profile_pic or args.profile_pic_only
download_posts = not (args.no_posts or args.stories_only or args.profile_pic_only) download_posts = not (args.no_posts or args.stories_only or args.profile_pic_only)
@@ -482,7 +553,9 @@ def main():
latest_stamps_file=args.latest_stamps, latest_stamps_file=args.latest_stamps,
max_count=int(args.count) if args.count is not None else None, max_count=int(args.count) if args.count is not None else None,
post_filter_str=args.post_filter, post_filter_str=args.post_filter,
storyitem_filter_str=args.storyitem_filter) storyitem_filter_str=args.storyitem_filter,
browser=args.load_cookies,
cookiefile=args.cookiefile)
loader.close() loader.close()
except InstaloaderException as err: except InstaloaderException as err:
raise SystemExit("Fatal error: %s" % err) from err raise SystemExit("Fatal error: %s" % err) from err
+10 -4
View File
@@ -991,7 +991,8 @@ class Instaloader:
max_count: Optional[int] = None, max_count: Optional[int] = None,
total_count: Optional[int] = None, total_count: Optional[int] = None,
owner_profile: Optional[Profile] = None, owner_profile: Optional[Profile] = None,
takewhile: Optional[Callable[[Post], bool]] = None) -> None: takewhile: Optional[Callable[[Post], bool]] = None,
possibly_pinned: int = 0) -> None:
""" """
Download the Posts returned by given Post Iterator. Download the Posts returned by given Post Iterator.
@@ -1003,6 +1004,9 @@ class Instaloader:
.. versionchanged:: 4.8 .. versionchanged:: 4.8
Add `takewhile` parameter. Add `takewhile` parameter.
.. versionchanged:: 4.10.3
Add `possibly_pinned` parameter.
:param posts: Post Iterator to loop through. :param posts: Post Iterator to loop through.
:param target: Target name. :param target: Target name.
:param fast_update: :option:`--fast-update`. :param fast_update: :option:`--fast-update`.
@@ -1011,6 +1015,8 @@ class Instaloader:
:param total_count: Total number of posts returned by given iterator. :param total_count: Total number of posts returned by given iterator.
:param owner_profile: Associated profile, if any. :param owner_profile: Associated profile, if any.
:param takewhile: Expression evaluated for each post. Once it returns false, downloading stops. :param takewhile: Expression evaluated for each post. Once it returns false, downloading stops.
:param possibly_pinned: Number of posts that might be pinned. These posts do not cause download
to stop even if they've already been downloaded.
""" """
displayed_count = (max_count if total_count is None or max_count is not None and max_count < total_count displayed_count = (max_count if total_count is None or max_count is not None and max_count < total_count
else total_count) else total_count)
@@ -1032,7 +1038,7 @@ class Instaloader:
) as (is_resuming, start_index): ) as (is_resuming, start_index):
for number, post in enumerate(posts, start=start_index + 1): for number, post in enumerate(posts, start=start_index + 1):
should_stop = not takewhile(post) should_stop = not takewhile(post)
if should_stop and post.is_pinned: if should_stop and number <= possibly_pinned:
continue continue
if (max_count is not None and number > max_count) or should_stop: if (max_count is not None and number > max_count) or should_stop:
break break
@@ -1066,7 +1072,7 @@ class Instaloader:
except PostChangedException: except PostChangedException:
post_changed = True post_changed = True
continue continue
if fast_update and not downloaded and not post_changed and not post.is_pinned: if fast_update and not downloaded and not post_changed and number > possibly_pinned:
# disengage fast_update for first post when resuming # disengage fast_update for first post when resuming
if not is_resuming or number > 0: if not is_resuming or number > 0:
break break
@@ -1488,7 +1494,7 @@ class Instaloader:
posts_to_download = profile.get_posts() posts_to_download = profile.get_posts()
self.posts_download_loop(posts_to_download, profile_name, fast_update, post_filter, self.posts_download_loop(posts_to_download, profile_name, fast_update, post_filter,
total_count=profile.mediacount, owner_profile=profile, total_count=profile.mediacount, owner_profile=profile,
takewhile=posts_takewhile) takewhile=posts_takewhile, possibly_pinned=3)
if latest_stamps is not None and posts_to_download.first_item is not None: if latest_stamps is not None and posts_to_download.first_item is not None:
latest_stamps.set_last_post_timestamp(profile_name, latest_stamps.set_last_post_timestamp(profile_name,
posts_to_download.first_item.date_local) posts_to_download.first_item.date_local)
+15 -17
View File
@@ -3,7 +3,6 @@ import json
import os import os
import pickle import pickle
import random import random
import re
import shutil import shutil
import sys import sys
import textwrap import textwrap
@@ -33,8 +32,8 @@ def copy_session(session: requests.Session, request_timeout: Optional[float] = N
def default_user_agent() -> str: def default_user_agent() -> str:
return 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' \ return ('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36' '(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36')
def default_iphone_headers() -> Dict[str, Any]: def default_iphone_headers() -> Dict[str, Any]:
@@ -211,6 +210,10 @@ class InstaloaderContext:
"""Not meant to be used directly, use :meth:`Instaloader.save_session`.""" """Not meant to be used directly, use :meth:`Instaloader.save_session`."""
return requests.utils.dict_from_cookiejar(self._session.cookies) return requests.utils.dict_from_cookiejar(self._session.cookies)
def update_cookies(self, cookie):
""".. versionadded:: 4.11"""
self._session.cookies.update(cookie)
def load_session(self, username, sessiondata): def load_session(self, username, sessiondata):
"""Not meant to be used directly, use :meth:`Instaloader.load_session`.""" """Not meant to be used directly, use :meth:`Instaloader.load_session`."""
session = requests.Session() session = requests.Session()
@@ -256,18 +259,23 @@ class InstaloaderContext:
# Override default timeout behavior. # Override default timeout behavior.
# Need to silence mypy bug for this. See: https://github.com/python/mypy/issues/2427 # Need to silence mypy bug for this. See: https://github.com/python/mypy/issues/2427
session.request = partial(session.request, timeout=self.request_timeout) # type: ignore session.request = partial(session.request, timeout=self.request_timeout) # type: ignore
csrf_json = self.get_json('accounts/login/', {}, session=session)
csrf_token = csrf_json['config']['csrf_token'] # Make a request to Instagram's root URL, which will set the session's csrftoken cookie
# Not using self.get_json() here, because we need to access the cookie
session.get('https://www.instagram.com/')
# Add session's csrftoken cookie to session headers
csrf_token = session.cookies.get_dict()['csrftoken']
session.headers.update({'X-CSRFToken': csrf_token}) session.headers.update({'X-CSRFToken': csrf_token})
# Not using self.get_json() here, because we need to access csrftoken cookie
self.do_sleep() self.do_sleep()
# Workaround credits to pgrimaud. # Workaround credits to pgrimaud.
# See: https://github.com/pgrimaud/instagram-user-feed/commit/96ad4cf54d1ad331b337f325c73e664999a6d066 # See: https://github.com/pgrimaud/instagram-user-feed/commit/96ad4cf54d1ad331b337f325c73e664999a6d066
enc_password = '#PWD_INSTAGRAM_BROWSER:0:{}:{}'.format(int(datetime.now().timestamp()), passwd) enc_password = '#PWD_INSTAGRAM_BROWSER:0:{}:{}'.format(int(datetime.now().timestamp()), passwd)
login = session.post('https://www.instagram.com/accounts/login/ajax/', login = session.post('https://www.instagram.com/api/v1/web/accounts/login/ajax/',
data={'enc_password': enc_password, 'username': user}, allow_redirects=True) data={'enc_password': enc_password, 'username': user}, allow_redirects=True)
try: try:
resp_json = login.json() resp_json = login.json()
except json.decoder.JSONDecodeError as err: except json.decoder.JSONDecodeError as err:
raise ConnectionException( raise ConnectionException(
"Login error: JSON decode fail, {} - {}.".format(login.status_code, login.reason) "Login error: JSON decode fail, {} - {}.".format(login.status_code, login.reason)
@@ -403,16 +411,6 @@ class InstaloaderContext:
raise TooManyRequestsException("429 Too Many Requests") raise TooManyRequestsException("429 Too Many Requests")
if resp.status_code != 200: if resp.status_code != 200:
raise ConnectionException("HTTP error code {}.".format(resp.status_code)) raise ConnectionException("HTTP error code {}.".format(resp.status_code))
is_html_query = not is_graphql_query and not "__a" in params and host == "www.instagram.com"
if is_html_query:
# Extract JSON from HTML response
match = re.search('(?<={"raw":").*?(?<!\\\\)(?=")', resp.text)
if match is None:
raise QueryReturnedNotFoundException("Could not find JSON data in html response.")
# Unescape escaped JSON string
unescaped_string = match.group(0).encode("utf-8").decode("unicode_escape")
resp_json = json.loads(unescaped_string)
return resp_json
else: else:
resp_json = resp.json() resp_json = resp.json()
if 'status' in resp_json and resp_json['status'] != "ok": if 'status' in resp_json and resp_json['status'] != "ok":
+4
View File
@@ -184,6 +184,10 @@ class NodeIterator(Iterator[T]):
""" """
return self._node_wrapper(self._first_node) if self._first_node is not None else None return self._node_wrapper(self._first_node) if self._first_node is not None else None
@staticmethod
def page_length() -> int:
return NodeIterator._graphql_page_length
def freeze(self) -> FrozenNodeIterator: def freeze(self) -> FrozenNodeIterator:
"""Freeze the iterator for later resuming.""" """Freeze the iterator for later resuming."""
remaining_data = None remaining_data = None
+155 -15
View File
@@ -43,19 +43,91 @@ PostCommentAnswer.owner.__doc__ = "Owner :class:`Profile` of the comment."
PostCommentAnswer.likes_count.__doc__ = "Number of likes on comment." PostCommentAnswer.likes_count.__doc__ = "Number of likes on comment."
class PostComment(NamedTuple): class PostComment:
id: int def __init__(self, context: 'InstaloaderContext', node: Dict[str, Any],
created_at_utc: datetime answers: Iterator['PostCommentAnswer'], post: 'Post'):
text: str self._context = context
owner: 'Profile' self._node = node
likes_count: int self._answers = answers
answers: Iterator[PostCommentAnswer] self._post = post
@classmethod
def from_iphone_struct(
cls,
context: "InstaloaderContext",
media: Dict[str, Any],
answers: Iterator["PostCommentAnswer"],
post: "Post",
):
return cls(
context=context,
node={
"id": int(media["pk"]),
"created_at": media["created_at"],
"text": media["text"],
"edge_liked_by": {
"count": media["comment_like_count"],
},
"iphone_struct": media,
},
answers=answers,
post=post,
)
for field in PostCommentAnswer._fields: @property
getattr(PostComment, field).__doc__ = getattr(PostCommentAnswer, field).__doc__ # pylint: disable=no-member def id(self) -> int:
PostComment.answers.__doc__ = r"Iterator which yields all :class:`PostCommentAnswer`\ s for the comment." """ ID number of comment. """
return self._node['id']
@property
def created_at_utc(self) -> datetime:
""" :class:`~datetime.datetime` when comment was created (UTC). """
return datetime.utcfromtimestamp(self._node['created_at'])
@property
def text(self):
""" Comment text. """
return self._node['text']
@property
def owner(self) -> "Profile":
""" Owner :class:`Profile` of the comment. """
if "iphone_struct" in self._node:
return Profile.from_iphone_struct(
self._context, self._node["iphone_struct"]["user"]
)
return Profile(self._context, self._node["owner"])
@property
def likes_count(self):
""" Number of likes on comment. """
return self._node.get('edge_liked_by', {}).get('count', 0)
@property
def answers(self) -> Iterator['PostCommentAnswer']:
""" Iterator which yields all :class:`PostCommentAnswer` for the comment. """
return self._answers
@property
def likes(self) -> Iterable['Profile']:
"""
Iterate over all likes of a comment. A :class:`Profile` instance of each like is yielded.
.. versionadded:: 4.11
"""
if self.likes_count != 0:
return NodeIterator(
self._context,
'5f0b1f6281e72053cbc07909c8d154ae',
lambda d: d['data']['comment']['edge_liked_by'],
lambda n: Profile(self._context, n),
{'comment_id': self.id},
'https://www.instagram.com/p/{0}/'.format(self._post.shortcode),
)
return []
def __repr__(self):
return f'<PostComment {self.id} of {self._post.shortcode}>'
class PostLocation(NamedTuple): class PostLocation(NamedTuple):
id: int id: int
@@ -545,11 +617,69 @@ class Post:
except KeyError: except KeyError:
return self._field('edge_media_to_comment', 'count') return self._field('edge_media_to_comment', 'count')
def _get_comments_via_iphone_endpoint(self) -> Iterable[PostComment]:
"""
Iterate over all comments of the post via an iPhone endpoint.
.. versionadded:: 4.10.3
fallback for :issue:`2125`.
"""
def _query(min_id=None):
pagination_params = {"min_id": min_id} if min_id is not None else {}
return self._context.get_iphone_json(
f"api/v1/media/{self.mediaid}/comments/",
{
"can_support_threading": "true",
"permalink_enabled": "false",
**pagination_params,
},
)
def _answers(comment_node):
def _answer(child_comment):
return PostCommentAnswer(
id=int(child_comment["pk"]),
created_at_utc=datetime.utcfromtimestamp(child_comment["created_at"]),
text=child_comment["text"],
owner=Profile.from_iphone_struct(self._context, child_comment["user"]),
likes_count=child_comment["comment_like_count"],
)
child_comment_count = comment_node["child_comment_count"]
if child_comment_count == 0:
return
preview_child_comments = comment_node["preview_child_comments"]
if child_comment_count == len(preview_child_comments):
yield from (
_answer(child_comment) for child_comment in preview_child_comments
)
return
pk = comment_node["pk"]
answers_json = self._context.get_iphone_json(
f"api/v1/media/{self.mediaid}/comments/{pk}/child_comments/",
{"max_id": ""},
)
yield from (
_answer(child_comment) for child_comment in answers_json["child_comments"]
)
def _paginated_comments(comments_json):
for comment_node in comments_json.get("comments", []):
yield PostComment.from_iphone_struct(
self._context, comment_node, _answers(comment_node), self
)
next_min_id = comments_json.get("next_min_id")
if next_min_id:
yield from _paginated_comments(_query(next_min_id))
return _paginated_comments(_query())
def get_comments(self) -> Iterable[PostComment]: def get_comments(self) -> Iterable[PostComment]:
r"""Iterate over all comments of the post. """Iterate over all comments of the post.
Each comment is represented by a PostComment NamedTuple with fields text (string), created_at (datetime), Each comment is represented by a PostComment NamedTuple with fields text (string), created_at (datetime),
id (int), owner (:class:`Profile`) and answers (:class:`~typing.Iterator`\ [:class:`PostCommentAnswer`]) id (int), owner (:class:`Profile`) and answers (:class:`~typing.Iterator` [:class:`PostCommentAnswer`])
if available. if available.
.. versionchanged:: 4.7 .. versionchanged:: 4.7
@@ -584,8 +714,8 @@ class Post:
) )
def _postcomment(node): def _postcomment(node):
return PostComment(*_postcommentanswer(node), return PostComment(context=self._context, node=node,
answers=_postcommentanswers(node)) answers=_postcommentanswers(node), post=self)
if self.comments == 0: if self.comments == 0:
# Avoid doing additional requests if there are no comments # Avoid doing additional requests if there are no comments
return [] return []
@@ -596,6 +726,12 @@ class Post:
if self.comments == len(comment_edges) + answers_count: if self.comments == len(comment_edges) + answers_count:
# If the Post's metadata already contains all parent comments, don't do GraphQL requests to obtain them # If the Post's metadata already contains all parent comments, don't do GraphQL requests to obtain them
return [_postcomment(comment['node']) for comment in comment_edges] return [_postcomment(comment['node']) for comment in comment_edges]
if self.comments > NodeIterator.page_length():
# comments pagination via our graphql query does not work reliably anymore (issue #2125), fallback to an
# iphone endpoint if needed.
return self._get_comments_via_iphone_endpoint()
return NodeIterator( return NodeIterator(
self._context, self._context,
'97b41c52301f77ce508f55e66d17620e', '97b41c52301f77ce508f55e66d17620e',
@@ -678,7 +814,11 @@ class Post:
@property @property
def is_pinned(self) -> bool: def is_pinned(self) -> bool:
"""True if this Post has been pinned by at least one user. """
.. deprecated: 4.10.3
This information is not returned by IG anymore
Used to return True if this Post has been pinned by at least one user, now likely returns always false.
.. versionadded: 4.9.2""" .. versionadded: 4.9.2"""
return 'pinned_for_users' in self._node and bool(self._node['pinned_for_users']) return 'pinned_for_users' in self._node and bool(self._node['pinned_for_users'])
+5
View File
@@ -22,6 +22,9 @@ if sys.version_info < (3, 8):
sys.exit('Instaloader requires Python >= 3.8.') sys.exit('Instaloader requires Python >= 3.8.')
requirements = ['requests>=2.4'] requirements = ['requests>=2.4']
optional_requirements = {
'browser_cookie3': ['browser_cookie3>=0.19.1'],
}
keywords = (['instagram', 'instagram-scraper', 'instagram-client', 'instagram-feed', 'downloader', 'videos', 'photos', keywords = (['instagram', 'instagram-scraper', 'instagram-client', 'instagram-feed', 'downloader', 'videos', 'photos',
'pictures', 'instagram-user-photos', 'instagram-photos', 'instagram-metadata', 'instagram-downloader', 'pictures', 'instagram-user-photos', 'instagram-photos', 'instagram-metadata', 'instagram-downloader',
@@ -44,6 +47,7 @@ setup(
long_description=open(os.path.join(SRC, 'README.rst')).read(), long_description=open(os.path.join(SRC, 'README.rst')).read(),
install_requires=requirements, install_requires=requirements,
python_requires='>=3.8', python_requires='>=3.8',
extras_require=optional_requirements,
entry_points={'console_scripts': ['instaloader=instaloader.__main__:main']}, entry_points={'console_scripts': ['instaloader=instaloader.__main__:main']},
zip_safe=False, zip_safe=False,
keywords=keywords, keywords=keywords,
@@ -58,6 +62,7 @@ setup(
'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3 :: Only',
'Topic :: Internet', 'Topic :: Internet',
'Topic :: Multimedia :: Graphics' 'Topic :: Multimedia :: Graphics'