mirror of
https://github.com/instaloader/instaloader.git
synced 2026-10-19 15:12:16 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d5bb1e181 | ||
|
|
819057eb6f | ||
|
|
5235cc0f6a | ||
|
|
fb06b4b7d6 | ||
|
|
1a382cd61c | ||
|
|
0345da71ec | ||
|
|
749ff60639 | ||
|
|
3528e8d19b | ||
|
|
5fce6946ea | ||
|
|
692cbc000d | ||
|
|
102f27c4ef | ||
|
|
33d13c4b32 | ||
|
|
408cf22c18 | ||
|
|
93882d93e0 | ||
|
|
8ef7b3242c | ||
|
|
406f33896a | ||
|
|
b9be1eaaf4 | ||
|
|
6b24b54591 | ||
|
|
67d3b4fca1 | ||
|
|
4c72077976 | ||
|
|
4f9d64a284 | ||
|
|
0c9a667da7 | ||
|
|
ccf55dbeef | ||
|
|
87d877e650 | ||
|
|
adb9f3a346 | ||
|
|
2aa26ea140 | ||
|
|
0fd06fe6bc | ||
|
|
15d9cd8949 | ||
|
|
9421ad5fda | ||
|
|
1e89ebe038 |
@@ -1 +1,2 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
|
.mypy_cache/
|
||||||
|
|||||||
@@ -0,0 +1,584 @@
|
|||||||
|
[MASTER]
|
||||||
|
|
||||||
|
# A comma-separated list of package or module names from where C extensions may
|
||||||
|
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||||
|
# run arbitrary code.
|
||||||
|
extension-pkg-whitelist=
|
||||||
|
|
||||||
|
# Add files or directories to the blacklist. They should be base names, not
|
||||||
|
# paths.
|
||||||
|
ignore=CVS
|
||||||
|
|
||||||
|
# Add files or directories matching the regex patterns to the blacklist. The
|
||||||
|
# regex matches against base names, not paths.
|
||||||
|
ignore-patterns=
|
||||||
|
|
||||||
|
# Python code to execute, usually for sys.path manipulation such as
|
||||||
|
# pygtk.require().
|
||||||
|
#init-hook=
|
||||||
|
|
||||||
|
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||||
|
# number of processors available to use.
|
||||||
|
jobs=0
|
||||||
|
|
||||||
|
# Control the amount of potential inferred values when inferring a single
|
||||||
|
# object. This can help the performance when dealing with large functions or
|
||||||
|
# complex, nested conditions.
|
||||||
|
limit-inference-results=100
|
||||||
|
|
||||||
|
# List of plugins (as comma separated values of python modules names) to load,
|
||||||
|
# usually to register additional checkers.
|
||||||
|
load-plugins=
|
||||||
|
|
||||||
|
# Pickle collected data for later comparisons.
|
||||||
|
persistent=yes
|
||||||
|
|
||||||
|
# Specify a configuration file.
|
||||||
|
#rcfile=
|
||||||
|
|
||||||
|
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||||
|
# user-friendly hints instead of false-positive error messages.
|
||||||
|
suggestion-mode=yes
|
||||||
|
|
||||||
|
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||||
|
# active Python interpreter and may run arbitrary code.
|
||||||
|
unsafe-load-any-extension=no
|
||||||
|
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
|
||||||
|
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||||
|
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
|
||||||
|
confidence=
|
||||||
|
|
||||||
|
# Disable the message, report, category or checker with the given id(s). You
|
||||||
|
# can either give multiple identifiers separated by comma (,) or put this
|
||||||
|
# option multiple times (only on the command line, not in the configuration
|
||||||
|
# file where it should appear only once). You can also use "--disable=all" to
|
||||||
|
# disable everything first and then reenable specific checks. For example, if
|
||||||
|
# you want to run only the similarities checker, you can use "--disable=all
|
||||||
|
# --enable=similarities". If you want to run only the classes checker, but have
|
||||||
|
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||||
|
# --disable=W".
|
||||||
|
disable=invalid-name,
|
||||||
|
missing-docstring,
|
||||||
|
too-many-lines,
|
||||||
|
print-statement,
|
||||||
|
parameter-unpacking,
|
||||||
|
unpacking-in-except,
|
||||||
|
old-raise-syntax,
|
||||||
|
backtick,
|
||||||
|
long-suffix,
|
||||||
|
old-ne-operator,
|
||||||
|
old-octal-literal,
|
||||||
|
import-star-module-level,
|
||||||
|
non-ascii-bytes-literal,
|
||||||
|
raw-checker-failed,
|
||||||
|
bad-inline-option,
|
||||||
|
locally-disabled,
|
||||||
|
file-ignored,
|
||||||
|
suppressed-message,
|
||||||
|
useless-suppression,
|
||||||
|
deprecated-pragma,
|
||||||
|
use-symbolic-message-instead,
|
||||||
|
too-many-instance-attributes,
|
||||||
|
too-many-public-methods,
|
||||||
|
too-many-branches,
|
||||||
|
too-many-arguments,
|
||||||
|
too-many-locals,
|
||||||
|
too-many-statements,
|
||||||
|
no-else-return,
|
||||||
|
no-else-raise,
|
||||||
|
unnecessary-pass,
|
||||||
|
wildcard-import,
|
||||||
|
unused-wildcard-import,
|
||||||
|
apply-builtin,
|
||||||
|
basestring-builtin,
|
||||||
|
buffer-builtin,
|
||||||
|
cmp-builtin,
|
||||||
|
coerce-builtin,
|
||||||
|
execfile-builtin,
|
||||||
|
file-builtin,
|
||||||
|
long-builtin,
|
||||||
|
raw_input-builtin,
|
||||||
|
reduce-builtin,
|
||||||
|
standarderror-builtin,
|
||||||
|
unicode-builtin,
|
||||||
|
xrange-builtin,
|
||||||
|
coerce-method,
|
||||||
|
delslice-method,
|
||||||
|
getslice-method,
|
||||||
|
setslice-method,
|
||||||
|
no-absolute-import,
|
||||||
|
old-division,
|
||||||
|
dict-iter-method,
|
||||||
|
dict-view-method,
|
||||||
|
next-method-called,
|
||||||
|
metaclass-assignment,
|
||||||
|
indexing-exception,
|
||||||
|
raising-string,
|
||||||
|
reload-builtin,
|
||||||
|
oct-method,
|
||||||
|
hex-method,
|
||||||
|
nonzero-method,
|
||||||
|
cmp-method,
|
||||||
|
input-builtin,
|
||||||
|
round-builtin,
|
||||||
|
intern-builtin,
|
||||||
|
unichr-builtin,
|
||||||
|
map-builtin-not-iterating,
|
||||||
|
zip-builtin-not-iterating,
|
||||||
|
range-builtin-not-iterating,
|
||||||
|
filter-builtin-not-iterating,
|
||||||
|
using-cmp-argument,
|
||||||
|
eq-without-hash,
|
||||||
|
div-method,
|
||||||
|
idiv-method,
|
||||||
|
rdiv-method,
|
||||||
|
exception-message-attribute,
|
||||||
|
invalid-str-codec,
|
||||||
|
sys-max-int,
|
||||||
|
bad-python3-import,
|
||||||
|
deprecated-string-function,
|
||||||
|
deprecated-str-translate-call,
|
||||||
|
deprecated-itertools-function,
|
||||||
|
deprecated-types-field,
|
||||||
|
next-method-defined,
|
||||||
|
dict-items-not-iterating,
|
||||||
|
dict-keys-not-iterating,
|
||||||
|
dict-values-not-iterating,
|
||||||
|
deprecated-operator-function,
|
||||||
|
deprecated-urllib-function,
|
||||||
|
xreadlines-attribute,
|
||||||
|
deprecated-sys-function,
|
||||||
|
exception-escape,
|
||||||
|
comprehension-escape
|
||||||
|
|
||||||
|
# Enable the message, report, category or checker with the given id(s). You can
|
||||||
|
# either give multiple identifier separated by comma (,) or put this option
|
||||||
|
# multiple time (only on the command line, not in the configuration file where
|
||||||
|
# it should appear only once). See also the "--disable" option for examples.
|
||||||
|
enable=c-extension-no-member
|
||||||
|
|
||||||
|
|
||||||
|
[REPORTS]
|
||||||
|
|
||||||
|
# Python expression which should return a note less than 10 (10 is the highest
|
||||||
|
# note). You have access to the variables errors warning, statement which
|
||||||
|
# respectively contain the number of errors / warnings messages and the total
|
||||||
|
# number of statements analyzed. This is used by the global evaluation report
|
||||||
|
# (RP0004).
|
||||||
|
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||||
|
|
||||||
|
# Template used to display messages. This is a python new-style format string
|
||||||
|
# used to format the message information. See doc for all details.
|
||||||
|
#msg-template=
|
||||||
|
|
||||||
|
# Set the output format. Available formats are text, parseable, colorized, json
|
||||||
|
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||||
|
# mypackage.mymodule.MyReporterClass.
|
||||||
|
output-format=text
|
||||||
|
|
||||||
|
# Tells whether to display a full report or only the messages.
|
||||||
|
reports=no
|
||||||
|
|
||||||
|
# Activate the evaluation score.
|
||||||
|
score=yes
|
||||||
|
|
||||||
|
|
||||||
|
[REFACTORING]
|
||||||
|
|
||||||
|
# Maximum number of nested blocks for function / method body
|
||||||
|
max-nested-blocks=5
|
||||||
|
|
||||||
|
# Complete name of functions that never returns. When checking for
|
||||||
|
# inconsistent-return-statements if a never returning function is called then
|
||||||
|
# it will be considered as an explicit return statement and no message will be
|
||||||
|
# printed.
|
||||||
|
never-returning-functions=sys.exit
|
||||||
|
|
||||||
|
|
||||||
|
[VARIABLES]
|
||||||
|
|
||||||
|
# List of additional names supposed to be defined in builtins. Remember that
|
||||||
|
# you should avoid defining new builtins when possible.
|
||||||
|
additional-builtins=
|
||||||
|
|
||||||
|
# Tells whether unused global variables should be treated as a violation.
|
||||||
|
allow-global-unused-variables=yes
|
||||||
|
|
||||||
|
# List of strings which can identify a callback function by name. A callback
|
||||||
|
# name must start or end with one of those strings.
|
||||||
|
callbacks=cb_,
|
||||||
|
_cb
|
||||||
|
|
||||||
|
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||||
|
# not be used).
|
||||||
|
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
|
||||||
|
|
||||||
|
# Argument names that match this expression will be ignored. Default to name
|
||||||
|
# with leading underscore.
|
||||||
|
ignored-argument-names=_.*|^ignored_|^unused_
|
||||||
|
|
||||||
|
# Tells whether we should check for unused import in __init__ files.
|
||||||
|
init-import=no
|
||||||
|
|
||||||
|
# List of qualified module names which can have objects that can redefine
|
||||||
|
# builtins.
|
||||||
|
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
|
||||||
|
|
||||||
|
|
||||||
|
[TYPECHECK]
|
||||||
|
|
||||||
|
# List of decorators that produce context managers, such as
|
||||||
|
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||||
|
# produce valid context managers.
|
||||||
|
contextmanager-decorators=contextlib.contextmanager
|
||||||
|
|
||||||
|
# List of members which are set dynamically and missed by pylint inference
|
||||||
|
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||||
|
# expressions are accepted.
|
||||||
|
generated-members=
|
||||||
|
|
||||||
|
# Tells whether missing members accessed in mixin class should be ignored. A
|
||||||
|
# mixin class is detected if its name ends with "mixin" (case insensitive).
|
||||||
|
ignore-mixin-members=yes
|
||||||
|
|
||||||
|
# Tells whether to warn about missing members when the owner of the attribute
|
||||||
|
# is inferred to be None.
|
||||||
|
ignore-none=yes
|
||||||
|
|
||||||
|
# This flag controls whether pylint should warn about no-member and similar
|
||||||
|
# checks whenever an opaque object is returned when inferring. The inference
|
||||||
|
# can return multiple potential results while evaluating a Python object, but
|
||||||
|
# some branches might not be evaluated, which results in partial inference. In
|
||||||
|
# that case, it might be useful to still emit no-member and other checks for
|
||||||
|
# the rest of the inferred objects.
|
||||||
|
ignore-on-opaque-inference=yes
|
||||||
|
|
||||||
|
# List of class names for which member attributes should not be checked (useful
|
||||||
|
# for classes with dynamically set attributes). This supports the use of
|
||||||
|
# qualified names.
|
||||||
|
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||||
|
|
||||||
|
# List of module names for which member attributes should not be checked
|
||||||
|
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||||
|
# and thus existing member attributes cannot be deduced by static analysis. It
|
||||||
|
# supports qualified module names, as well as Unix pattern matching.
|
||||||
|
ignored-modules=
|
||||||
|
|
||||||
|
# Show a hint with possible names when a member name was not found. The aspect
|
||||||
|
# of finding the hint is based on edit distance.
|
||||||
|
missing-member-hint=yes
|
||||||
|
|
||||||
|
# The minimum edit distance a name should have in order to be considered a
|
||||||
|
# similar match for a missing member name.
|
||||||
|
missing-member-hint-distance=1
|
||||||
|
|
||||||
|
# The total number of similar names that should be taken in consideration when
|
||||||
|
# showing a hint for a missing member.
|
||||||
|
missing-member-max-choices=1
|
||||||
|
|
||||||
|
|
||||||
|
[STRING]
|
||||||
|
|
||||||
|
# This flag controls whether the implicit-str-concat-in-sequence should
|
||||||
|
# generate a warning on implicit string concatenation in sequences defined over
|
||||||
|
# several lines.
|
||||||
|
check-str-concat-over-line-jumps=no
|
||||||
|
|
||||||
|
|
||||||
|
[SPELLING]
|
||||||
|
|
||||||
|
# Limits count of emitted suggestions for spelling mistakes.
|
||||||
|
max-spelling-suggestions=4
|
||||||
|
|
||||||
|
# Spelling dictionary name. Available dictionaries: none. To make it working
|
||||||
|
# install python-enchant package..
|
||||||
|
spelling-dict=
|
||||||
|
|
||||||
|
# List of comma separated words that should not be checked.
|
||||||
|
spelling-ignore-words=
|
||||||
|
|
||||||
|
# A path to a file that contains private dictionary; one word per line.
|
||||||
|
spelling-private-dict-file=
|
||||||
|
|
||||||
|
# Tells whether to store unknown words to indicated private dictionary in
|
||||||
|
# --spelling-private-dict-file option instead of raising a message.
|
||||||
|
spelling-store-unknown-words=no
|
||||||
|
|
||||||
|
|
||||||
|
[SIMILARITIES]
|
||||||
|
|
||||||
|
# Ignore comments when computing similarities.
|
||||||
|
ignore-comments=yes
|
||||||
|
|
||||||
|
# Ignore docstrings when computing similarities.
|
||||||
|
ignore-docstrings=yes
|
||||||
|
|
||||||
|
# Ignore imports when computing similarities.
|
||||||
|
ignore-imports=no
|
||||||
|
|
||||||
|
# Minimum lines number of a similarity.
|
||||||
|
min-similarity-lines=4
|
||||||
|
|
||||||
|
|
||||||
|
[MISCELLANEOUS]
|
||||||
|
|
||||||
|
# List of note tags to take in consideration, separated by a comma.
|
||||||
|
notes=FIXME,
|
||||||
|
XXX,
|
||||||
|
TODO
|
||||||
|
|
||||||
|
|
||||||
|
[LOGGING]
|
||||||
|
|
||||||
|
# Format style used to check logging format string. `old` means using %
|
||||||
|
# formatting, while `new` is for `{}` formatting.
|
||||||
|
logging-format-style=old
|
||||||
|
|
||||||
|
# Logging modules to check that the string format arguments are in logging
|
||||||
|
# function parameter format.
|
||||||
|
logging-modules=logging
|
||||||
|
|
||||||
|
|
||||||
|
[FORMAT]
|
||||||
|
|
||||||
|
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||||
|
expected-line-ending-format=
|
||||||
|
|
||||||
|
# Regexp for a line that is allowed to be longer than the limit.
|
||||||
|
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||||
|
|
||||||
|
# Number of spaces of indent required inside a hanging or continued line.
|
||||||
|
indent-after-paren=4
|
||||||
|
|
||||||
|
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||||
|
# tab).
|
||||||
|
indent-string=' '
|
||||||
|
|
||||||
|
# Maximum number of characters on a single line.
|
||||||
|
max-line-length=120
|
||||||
|
|
||||||
|
# Maximum number of lines in a module.
|
||||||
|
max-module-lines=1000
|
||||||
|
|
||||||
|
# List of optional constructs for which whitespace checking is disabled. `dict-
|
||||||
|
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
|
||||||
|
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
|
||||||
|
# `empty-line` allows space-only lines.
|
||||||
|
no-space-check=trailing-comma,
|
||||||
|
dict-separator
|
||||||
|
|
||||||
|
# Allow the body of a class to be on the same line as the declaration if body
|
||||||
|
# contains single statement.
|
||||||
|
single-line-class-stmt=no
|
||||||
|
|
||||||
|
# Allow the body of an if to be on the same line as the test if there is no
|
||||||
|
# else.
|
||||||
|
single-line-if-stmt=no
|
||||||
|
|
||||||
|
|
||||||
|
[BASIC]
|
||||||
|
|
||||||
|
# Naming style matching correct argument names.
|
||||||
|
argument-naming-style=snake_case
|
||||||
|
|
||||||
|
# Regular expression matching correct argument names. Overrides argument-
|
||||||
|
# naming-style.
|
||||||
|
#argument-rgx=
|
||||||
|
|
||||||
|
# Naming style matching correct attribute names.
|
||||||
|
attr-naming-style=snake_case
|
||||||
|
|
||||||
|
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||||
|
# style.
|
||||||
|
#attr-rgx=
|
||||||
|
|
||||||
|
# Bad variable names which should always be refused, separated by a comma.
|
||||||
|
bad-names=foo,
|
||||||
|
bar,
|
||||||
|
baz,
|
||||||
|
toto,
|
||||||
|
tutu,
|
||||||
|
tata
|
||||||
|
|
||||||
|
# Naming style matching correct class attribute names.
|
||||||
|
class-attribute-naming-style=any
|
||||||
|
|
||||||
|
# Regular expression matching correct class attribute names. Overrides class-
|
||||||
|
# attribute-naming-style.
|
||||||
|
#class-attribute-rgx=
|
||||||
|
|
||||||
|
# Naming style matching correct class names.
|
||||||
|
class-naming-style=PascalCase
|
||||||
|
|
||||||
|
# Regular expression matching correct class names. Overrides class-naming-
|
||||||
|
# style.
|
||||||
|
#class-rgx=
|
||||||
|
|
||||||
|
# Naming style matching correct constant names.
|
||||||
|
const-naming-style=UPPER_CASE
|
||||||
|
|
||||||
|
# Regular expression matching correct constant names. Overrides const-naming-
|
||||||
|
# style.
|
||||||
|
#const-rgx=
|
||||||
|
|
||||||
|
# Minimum line length for functions/classes that require docstrings, shorter
|
||||||
|
# ones are exempt.
|
||||||
|
docstring-min-length=-1
|
||||||
|
|
||||||
|
# Naming style matching correct function names.
|
||||||
|
function-naming-style=snake_case
|
||||||
|
|
||||||
|
# Regular expression matching correct function names. Overrides function-
|
||||||
|
# naming-style.
|
||||||
|
#function-rgx=
|
||||||
|
|
||||||
|
# Good variable names which should always be accepted, separated by a comma.
|
||||||
|
good-names=i,
|
||||||
|
j,
|
||||||
|
k,
|
||||||
|
ex,
|
||||||
|
Run,
|
||||||
|
_
|
||||||
|
|
||||||
|
# Include a hint for the correct naming format with invalid-name.
|
||||||
|
include-naming-hint=no
|
||||||
|
|
||||||
|
# Naming style matching correct inline iteration names.
|
||||||
|
inlinevar-naming-style=any
|
||||||
|
|
||||||
|
# Regular expression matching correct inline iteration names. Overrides
|
||||||
|
# inlinevar-naming-style.
|
||||||
|
#inlinevar-rgx=
|
||||||
|
|
||||||
|
# Naming style matching correct method names.
|
||||||
|
method-naming-style=snake_case
|
||||||
|
|
||||||
|
# Regular expression matching correct method names. Overrides method-naming-
|
||||||
|
# style.
|
||||||
|
#method-rgx=
|
||||||
|
|
||||||
|
# Naming style matching correct module names.
|
||||||
|
module-naming-style=snake_case
|
||||||
|
|
||||||
|
# Regular expression matching correct module names. Overrides module-naming-
|
||||||
|
# style.
|
||||||
|
#module-rgx=
|
||||||
|
|
||||||
|
# Colon-delimited sets of names that determine each other's naming style when
|
||||||
|
# the name regexes allow several styles.
|
||||||
|
name-group=
|
||||||
|
|
||||||
|
# Regular expression which should only match function or class names that do
|
||||||
|
# not require a docstring.
|
||||||
|
no-docstring-rgx=^_
|
||||||
|
|
||||||
|
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||||
|
# to this list to register other decorators that produce valid properties.
|
||||||
|
# These decorators are taken in consideration only for invalid-name.
|
||||||
|
property-classes=abc.abstractproperty
|
||||||
|
|
||||||
|
# Naming style matching correct variable names.
|
||||||
|
variable-naming-style=snake_case
|
||||||
|
|
||||||
|
# Regular expression matching correct variable names. Overrides variable-
|
||||||
|
# naming-style.
|
||||||
|
#variable-rgx=
|
||||||
|
|
||||||
|
|
||||||
|
[IMPORTS]
|
||||||
|
|
||||||
|
# Allow wildcard imports from modules that define __all__.
|
||||||
|
allow-wildcard-with-all=no
|
||||||
|
|
||||||
|
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||||
|
# 3 compatible code, which means that the block might have code that exists
|
||||||
|
# only in one or another interpreter, leading to false positives when analysed.
|
||||||
|
analyse-fallback-blocks=no
|
||||||
|
|
||||||
|
# Deprecated modules which should not be used, separated by a comma.
|
||||||
|
deprecated-modules=optparse,tkinter.tix
|
||||||
|
|
||||||
|
# Create a graph of external dependencies in the given file (report RP0402 must
|
||||||
|
# not be disabled).
|
||||||
|
ext-import-graph=
|
||||||
|
|
||||||
|
# Create a graph of every (i.e. internal and external) dependencies in the
|
||||||
|
# given file (report RP0402 must not be disabled).
|
||||||
|
import-graph=
|
||||||
|
|
||||||
|
# Create a graph of internal dependencies in the given file (report RP0402 must
|
||||||
|
# not be disabled).
|
||||||
|
int-import-graph=
|
||||||
|
|
||||||
|
# Force import order to recognize a module as part of the standard
|
||||||
|
# compatibility libraries.
|
||||||
|
known-standard-library=
|
||||||
|
|
||||||
|
# Force import order to recognize a module as part of a third party library.
|
||||||
|
known-third-party=enchant
|
||||||
|
|
||||||
|
|
||||||
|
[DESIGN]
|
||||||
|
|
||||||
|
# Maximum number of arguments for function / method.
|
||||||
|
max-args=5
|
||||||
|
|
||||||
|
# Maximum number of attributes for a class (see R0902).
|
||||||
|
max-attributes=7
|
||||||
|
|
||||||
|
# Maximum number of boolean expressions in an if statement.
|
||||||
|
max-bool-expr=5
|
||||||
|
|
||||||
|
# Maximum number of branch for function / method body.
|
||||||
|
max-branches=12
|
||||||
|
|
||||||
|
# Maximum number of locals for function / method body.
|
||||||
|
max-locals=15
|
||||||
|
|
||||||
|
# Maximum number of parents for a class (see R0901).
|
||||||
|
max-parents=7
|
||||||
|
|
||||||
|
# Maximum number of public methods for a class (see R0904).
|
||||||
|
max-public-methods=20
|
||||||
|
|
||||||
|
# Maximum number of return / yield for function / method body.
|
||||||
|
max-returns=6
|
||||||
|
|
||||||
|
# Maximum number of statements in function / method body.
|
||||||
|
max-statements=50
|
||||||
|
|
||||||
|
# Minimum number of public methods for a class (see R0903).
|
||||||
|
min-public-methods=2
|
||||||
|
|
||||||
|
|
||||||
|
[CLASSES]
|
||||||
|
|
||||||
|
# List of method names used to declare (i.e. assign) instance attributes.
|
||||||
|
defining-attr-methods=__init__,
|
||||||
|
__new__,
|
||||||
|
setUp
|
||||||
|
|
||||||
|
# List of member names, which should be excluded from the protected access
|
||||||
|
# warning.
|
||||||
|
exclude-protected=_asdict,
|
||||||
|
_fields,
|
||||||
|
_replace,
|
||||||
|
_source,
|
||||||
|
_make
|
||||||
|
|
||||||
|
# List of valid names for the first argument in a class method.
|
||||||
|
valid-classmethod-first-arg=cls
|
||||||
|
|
||||||
|
# List of valid names for the first argument in a metaclass class method.
|
||||||
|
valid-metaclass-classmethod-first-arg=cls
|
||||||
|
|
||||||
|
|
||||||
|
[EXCEPTIONS]
|
||||||
|
|
||||||
|
# Exceptions that will emit a warning when being caught. Defaults to
|
||||||
|
# "BaseException, Exception".
|
||||||
|
overgeneral-exceptions=BaseException,
|
||||||
|
Exception
|
||||||
+13
-5
@@ -1,15 +1,18 @@
|
|||||||
|
dist: xenial
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- "3.5"
|
- "3.5"
|
||||||
- "3.5-dev"
|
|
||||||
- "3.6"
|
- "3.6"
|
||||||
- "3.6-dev"
|
- "3.7"
|
||||||
|
- "3.8-dev"
|
||||||
install:
|
install:
|
||||||
- pip install pylint requests
|
- pipenv sync --dev
|
||||||
- pip install -r docs/requirements.txt
|
|
||||||
script:
|
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,invalid-name,wildcard-import,unused-wildcard-import,no-else-return,cyclic-import,unnecessary-pass instaloader
|
- pylint instaloader
|
||||||
|
- mypy -m instaloader
|
||||||
- make -C docs html SPHINXOPTS="-W -n"
|
- make -C docs html SPHINXOPTS="-W -n"
|
||||||
|
addons:
|
||||||
|
ssh_known_hosts: aur.archlinux.org
|
||||||
deploy:
|
deploy:
|
||||||
- provider: pypi
|
- provider: pypi
|
||||||
user: aandergr
|
user: aandergr
|
||||||
@@ -27,3 +30,8 @@ deploy:
|
|||||||
on:
|
on:
|
||||||
branch: master
|
branch: master
|
||||||
python: 3.6
|
python: 3.6
|
||||||
|
- provider: script
|
||||||
|
script: deploy/arch/deploy.sh $TRAVIS_TAG
|
||||||
|
on:
|
||||||
|
tags: true
|
||||||
|
python: 3.6
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[[source]]
|
||||||
|
name = "pypi"
|
||||||
|
url = "https://pypi.org/simple"
|
||||||
|
verify_ssl = true
|
||||||
|
|
||||||
|
[dev-packages]
|
||||||
|
pylint = "*"
|
||||||
|
mypy = "*"
|
||||||
|
sphinx = "*"
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
requests = "*"
|
||||||
Generated
+360
@@ -0,0 +1,360 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"hash": {
|
||||||
|
"sha256": "dfbaa1642735b07bdcc118cb0c9328817b1d558af626b696593db099df863b39"
|
||||||
|
},
|
||||||
|
"pipfile-spec": 6,
|
||||||
|
"requires": {},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"name": "pypi",
|
||||||
|
"url": "https://pypi.org/simple",
|
||||||
|
"verify_ssl": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
|
||||||
|
"sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"
|
||||||
|
],
|
||||||
|
"version": "==2019.3.9"
|
||||||
|
},
|
||||||
|
"chardet": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||||
|
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||||
|
],
|
||||||
|
"version": "==3.0.4"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||||
|
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||||
|
],
|
||||||
|
"version": "==2.8"
|
||||||
|
},
|
||||||
|
"requests": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
|
||||||
|
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.22.0"
|
||||||
|
},
|
||||||
|
"urllib3": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
|
||||||
|
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
|
||||||
|
],
|
||||||
|
"version": "==1.25.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"develop": {
|
||||||
|
"alabaster": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
|
||||||
|
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
|
||||||
|
],
|
||||||
|
"version": "==0.7.12"
|
||||||
|
},
|
||||||
|
"astroid": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4",
|
||||||
|
"sha256:b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4"
|
||||||
|
],
|
||||||
|
"version": "==2.2.5"
|
||||||
|
},
|
||||||
|
"babel": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab",
|
||||||
|
"sha256:e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"
|
||||||
|
],
|
||||||
|
"version": "==2.7.0"
|
||||||
|
},
|
||||||
|
"certifi": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
|
||||||
|
"sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"
|
||||||
|
],
|
||||||
|
"version": "==2019.3.9"
|
||||||
|
},
|
||||||
|
"chardet": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||||
|
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||||
|
],
|
||||||
|
"version": "==3.0.4"
|
||||||
|
},
|
||||||
|
"docutils": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
|
||||||
|
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
|
||||||
|
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
|
||||||
|
],
|
||||||
|
"version": "==0.14"
|
||||||
|
},
|
||||||
|
"idna": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
|
||||||
|
"sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
|
||||||
|
],
|
||||||
|
"version": "==2.8"
|
||||||
|
},
|
||||||
|
"imagesize": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
|
||||||
|
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
|
||||||
|
],
|
||||||
|
"version": "==1.1.0"
|
||||||
|
},
|
||||||
|
"isort": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c40744b6bc5162bbb39c1257fe298b7a393861d50978b565f3ccd9cb9de0182a",
|
||||||
|
"sha256:f57abacd059dc3bd666258d1efb0377510a89777fda3e3274e3c01f7c03ae22d"
|
||||||
|
],
|
||||||
|
"version": "==4.3.20"
|
||||||
|
},
|
||||||
|
"jinja2": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013",
|
||||||
|
"sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"
|
||||||
|
],
|
||||||
|
"version": "==2.10.1"
|
||||||
|
},
|
||||||
|
"lazy-object-proxy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:159a745e61422217881c4de71f9eafd9d703b93af95618635849fe469a283661",
|
||||||
|
"sha256:23f63c0821cc96a23332e45dfaa83266feff8adc72b9bcaef86c202af765244f",
|
||||||
|
"sha256:3b11be575475db2e8a6e11215f5aa95b9ec14de658628776e10d96fa0b4dac13",
|
||||||
|
"sha256:3f447aff8bc61ca8b42b73304f6a44fa0d915487de144652816f950a3f1ab821",
|
||||||
|
"sha256:4ba73f6089cd9b9478bc0a4fa807b47dbdb8fad1d8f31a0f0a5dbf26a4527a71",
|
||||||
|
"sha256:4f53eadd9932055eac465bd3ca1bd610e4d7141e1278012bd1f28646aebc1d0e",
|
||||||
|
"sha256:64483bd7154580158ea90de5b8e5e6fc29a16a9b4db24f10193f0c1ae3f9d1ea",
|
||||||
|
"sha256:6f72d42b0d04bfee2397aa1862262654b56922c20a9bb66bb76b6f0e5e4f9229",
|
||||||
|
"sha256:7c7f1ec07b227bdc561299fa2328e85000f90179a2f44ea30579d38e037cb3d4",
|
||||||
|
"sha256:7c8b1ba1e15c10b13cad4171cfa77f5bb5ec2580abc5a353907780805ebe158e",
|
||||||
|
"sha256:8559b94b823f85342e10d3d9ca4ba5478168e1ac5658a8a2f18c991ba9c52c20",
|
||||||
|
"sha256:a262c7dfb046f00e12a2bdd1bafaed2408114a89ac414b0af8755c696eb3fc16",
|
||||||
|
"sha256:acce4e3267610c4fdb6632b3886fe3f2f7dd641158a843cf6b6a68e4ce81477b",
|
||||||
|
"sha256:be089bb6b83fac7f29d357b2dc4cf2b8eb8d98fe9d9ff89f9ea6012970a853c7",
|
||||||
|
"sha256:bfab710d859c779f273cc48fb86af38d6e9210f38287df0069a63e40b45a2f5c",
|
||||||
|
"sha256:c10d29019927301d524a22ced72706380de7cfc50f767217485a912b4c8bd82a",
|
||||||
|
"sha256:dd6e2b598849b3d7aee2295ac765a578879830fb8966f70be8cd472e6069932e",
|
||||||
|
"sha256:e408f1eacc0a68fed0c08da45f31d0ebb38079f043328dce69ff133b95c29dc1"
|
||||||
|
],
|
||||||
|
"version": "==1.4.1"
|
||||||
|
},
|
||||||
|
"markupsafe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473",
|
||||||
|
"sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161",
|
||||||
|
"sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235",
|
||||||
|
"sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5",
|
||||||
|
"sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff",
|
||||||
|
"sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b",
|
||||||
|
"sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1",
|
||||||
|
"sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e",
|
||||||
|
"sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183",
|
||||||
|
"sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66",
|
||||||
|
"sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1",
|
||||||
|
"sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1",
|
||||||
|
"sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e",
|
||||||
|
"sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b",
|
||||||
|
"sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905",
|
||||||
|
"sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735",
|
||||||
|
"sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d",
|
||||||
|
"sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e",
|
||||||
|
"sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d",
|
||||||
|
"sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c",
|
||||||
|
"sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21",
|
||||||
|
"sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2",
|
||||||
|
"sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5",
|
||||||
|
"sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b",
|
||||||
|
"sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6",
|
||||||
|
"sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f",
|
||||||
|
"sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f",
|
||||||
|
"sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"
|
||||||
|
],
|
||||||
|
"version": "==1.1.1"
|
||||||
|
},
|
||||||
|
"mccabe": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
|
||||||
|
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
|
||||||
|
],
|
||||||
|
"version": "==0.6.1"
|
||||||
|
},
|
||||||
|
"mypy": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2afe51527b1f6cdc4a5f34fc90473109b22bf7f21086ba3e9451857cf11489e6",
|
||||||
|
"sha256:56a16df3e0abb145d8accd5dbb70eba6c4bd26e2f89042b491faa78c9635d1e2",
|
||||||
|
"sha256:5764f10d27b2e93c84f70af5778941b8f4aa1379b2430f85c827e0f5464e8714",
|
||||||
|
"sha256:5bbc86374f04a3aa817622f98e40375ccb28c4836f36b66706cf3c6ccce86eda",
|
||||||
|
"sha256:6a9343089f6377e71e20ca734cd8e7ac25d36478a9df580efabfe9059819bf82",
|
||||||
|
"sha256:6c9851bc4a23dc1d854d3f5dfd5f20a016f8da86bcdbb42687879bb5f86434b0",
|
||||||
|
"sha256:b8e85956af3fcf043d6f87c91cbe8705073fc67029ba6e22d3468bfee42c4823",
|
||||||
|
"sha256:b9a0af8fae490306bc112229000aa0c2ccc837b49d29a5c42e088c132a2334dd",
|
||||||
|
"sha256:bbf643528e2a55df2c1587008d6e3bda5c0445f1240dfa85129af22ae16d7a9a",
|
||||||
|
"sha256:c46ab3438bd21511db0f2c612d89d8344154c0c9494afc7fbc932de514cf8d15",
|
||||||
|
"sha256:f7a83d6bd805855ef83ec605eb01ab4fa42bcef254b13631e451cbb44914a9b0"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.701"
|
||||||
|
},
|
||||||
|
"mypy-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812",
|
||||||
|
"sha256:b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e"
|
||||||
|
],
|
||||||
|
"version": "==0.4.1"
|
||||||
|
},
|
||||||
|
"packaging": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0c98a5d0be38ed775798ece1b9727178c4469d9c3b4ada66e8e6b7849f8732af",
|
||||||
|
"sha256:9e1cbf8c12b1f1ce0bb5344b8d7ecf66a6f8a6e91bcb0c84593ed6d3ab5c4ab3"
|
||||||
|
],
|
||||||
|
"version": "==19.0"
|
||||||
|
},
|
||||||
|
"pygments": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:36586500a94cd97f8c2c19d251cdb78868d1a822e0e491bfc1d811766aedb772",
|
||||||
|
"sha256:b437bc0d04dc36f1f5b3592985b3e0a3d0af46b7c39199231706d19a4ee63344"
|
||||||
|
],
|
||||||
|
"version": "==2.4.1"
|
||||||
|
},
|
||||||
|
"pylint": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09",
|
||||||
|
"sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.3.1"
|
||||||
|
},
|
||||||
|
"pyparsing": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a",
|
||||||
|
"sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03"
|
||||||
|
],
|
||||||
|
"version": "==2.4.0"
|
||||||
|
},
|
||||||
|
"pytz": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda",
|
||||||
|
"sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"
|
||||||
|
],
|
||||||
|
"version": "==2019.1"
|
||||||
|
},
|
||||||
|
"requests": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
|
||||||
|
"sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.22.0"
|
||||||
|
},
|
||||||
|
"six": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
|
||||||
|
"sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
|
||||||
|
],
|
||||||
|
"version": "==1.12.0"
|
||||||
|
},
|
||||||
|
"snowballstemmer": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
|
||||||
|
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
|
||||||
|
],
|
||||||
|
"version": "==1.2.1"
|
||||||
|
},
|
||||||
|
"sphinx": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:423280646fb37944dd3c85c58fb92a20d745793a9f6c511f59da82fa97cd404b",
|
||||||
|
"sha256:de930f42600a4fef993587633984cc5027dedba2464bcf00ddace26b40f8d9ce"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.0.1"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-applehelp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:edaa0ab2b2bc74403149cb0209d6775c96de797dfd5b5e2a71981309efab3897",
|
||||||
|
"sha256:fb8dee85af95e5c30c91f10e7eb3c8967308518e0f7488a2828ef7bc191d0d5d"
|
||||||
|
],
|
||||||
|
"version": "==1.0.1"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-devhelp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:6c64b077937330a9128a4da74586e8c2130262f014689b4b89e2d08ee7294a34",
|
||||||
|
"sha256:9512ecb00a2b0821a146736b39f7aeb90759834b07e81e8cc23a9c70bacb9981"
|
||||||
|
],
|
||||||
|
"version": "==1.0.1"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-htmlhelp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4670f99f8951bd78cd4ad2ab962f798f5618b17675c35c5ac3b2132a14ea8422",
|
||||||
|
"sha256:d4fd39a65a625c9df86d7fa8a2d9f3cd8299a3a4b15db63b50aac9e161d8eff7"
|
||||||
|
],
|
||||||
|
"version": "==1.0.2"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-jsmath": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178",
|
||||||
|
"sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"
|
||||||
|
],
|
||||||
|
"version": "==1.0.1"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-qthelp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:513049b93031beb1f57d4daea74068a4feb77aa5630f856fcff2e50de14e9a20",
|
||||||
|
"sha256:79465ce11ae5694ff165becda529a600c754f4bc459778778c7017374d4d406f"
|
||||||
|
],
|
||||||
|
"version": "==1.0.2"
|
||||||
|
},
|
||||||
|
"sphinxcontrib-serializinghtml": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:c0efb33f8052c04fd7a26c0a07f1678e8512e0faec19f4aa8f2473a8b81d5227",
|
||||||
|
"sha256:db6615af393650bf1151a6cd39120c29abaf93cc60db8c48eb2dddbfdc3a9768"
|
||||||
|
],
|
||||||
|
"version": "==1.1.3"
|
||||||
|
},
|
||||||
|
"typed-ast": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:132eae51d6ef3ff4a8c47c393a4ef5ebf0d1aecc96880eb5d6c8ceab7017cc9b",
|
||||||
|
"sha256:18141c1484ab8784006c839be8b985cfc82a2e9725837b0ecfa0203f71c4e39d",
|
||||||
|
"sha256:2baf617f5bbbfe73fd8846463f5aeafc912b5ee247f410700245d68525ec584a",
|
||||||
|
"sha256:3d90063f2cbbe39177e9b4d888e45777012652d6110156845b828908c51ae462",
|
||||||
|
"sha256:4304b2218b842d610aa1a1d87e1dc9559597969acc62ce717ee4dfeaa44d7eee",
|
||||||
|
"sha256:4983ede548ffc3541bae49a82675996497348e55bafd1554dc4e4a5d6eda541a",
|
||||||
|
"sha256:5315f4509c1476718a4825f45a203b82d7fdf2a6f5f0c8f166435975b1c9f7d4",
|
||||||
|
"sha256:6cdfb1b49d5345f7c2b90d638822d16ba62dc82f7616e9b4caa10b72f3f16649",
|
||||||
|
"sha256:7b325f12635598c604690efd7a0197d0b94b7d7778498e76e0710cd582fd1c7a",
|
||||||
|
"sha256:8d3b0e3b8626615826f9a626548057c5275a9733512b137984a68ba1598d3d2f",
|
||||||
|
"sha256:8f8631160c79f53081bd23446525db0bc4c5616f78d04021e6e434b286493fd7",
|
||||||
|
"sha256:912de10965f3dc89da23936f1cc4ed60764f712e5fa603a09dd904f88c996760",
|
||||||
|
"sha256:b010c07b975fe853c65d7bbe9d4ac62f1c69086750a574f6292597763781ba18",
|
||||||
|
"sha256:c908c10505904c48081a5415a1e295d8403e353e0c14c42b6d67f8f97fae6616",
|
||||||
|
"sha256:c94dd3807c0c0610f7c76f078119f4ea48235a953512752b9175f9f98f5ae2bd",
|
||||||
|
"sha256:ce65dee7594a84c466e79d7fb7d3303e7295d16a83c22c7c4037071b059e2c21",
|
||||||
|
"sha256:eaa9cfcb221a8a4c2889be6f93da141ac777eb8819f077e1d09fb12d00a09a93",
|
||||||
|
"sha256:f3376bc31bad66d46d44b4e6522c5c21976bf9bca4ef5987bb2bf727f4506cbb",
|
||||||
|
"sha256:f9202fa138544e13a4ec1a6792c35834250a85958fde1251b6a22e07d1260ae7"
|
||||||
|
],
|
||||||
|
"markers": "implementation_name == 'cpython'",
|
||||||
|
"version": "==1.3.5"
|
||||||
|
},
|
||||||
|
"urllib3": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:b246607a25ac80bedac05c6f282e3cdaf3afb65420fd024ac94435cabe6e18d1",
|
||||||
|
"sha256:dbe59173209418ae49d485b87d1681aefa36252ee85884c31346debd19463232"
|
||||||
|
],
|
||||||
|
"version": "==1.25.3"
|
||||||
|
},
|
||||||
|
"wrapt": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533"
|
||||||
|
],
|
||||||
|
"version": "==1.11.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -1,5 +1,41 @@
|
|||||||
.. image:: https://raw.githubusercontent.com/instaloader/instaloader/master/docs/logo_heading.png
|
.. image:: https://raw.githubusercontent.com/instaloader/instaloader/master/docs/logo_heading.png
|
||||||
|
|
||||||
|
.. badges-start
|
||||||
|
|
||||||
|
|travis| |pypi| |pyversion| |license| |aur| |contributors| |downloads| |saythanks|
|
||||||
|
|
||||||
|
.. |travis| image:: https://img.shields.io/travis/instaloader/instaloader/master.svg
|
||||||
|
:alt: Travis-CI Build Status
|
||||||
|
:target: https://travis-ci.org/instaloader/instaloader
|
||||||
|
|
||||||
|
.. |pypi| image:: https://img.shields.io/pypi/v/instaloader.svg
|
||||||
|
:alt: Instaloader PyPI Project Page
|
||||||
|
:target: https://pypi.org/project/instaloader/
|
||||||
|
|
||||||
|
.. |license| image:: https://img.shields.io/github/license/instaloader/instaloader.svg
|
||||||
|
:alt: MIT License
|
||||||
|
:target: https://github.com/instaloader/instaloader/blob/master/LICENSE
|
||||||
|
|
||||||
|
.. |pyversion| image:: https://img.shields.io/pypi/pyversions/instaloader.svg
|
||||||
|
:alt: Supported Python Versions
|
||||||
|
|
||||||
|
.. |contributors| image:: https://img.shields.io/github/contributors/instaloader/instaloader.svg
|
||||||
|
:alt: Contributor Count
|
||||||
|
:target: https://github.com/instaloader/instaloader/graphs/contributors
|
||||||
|
|
||||||
|
.. |saythanks| image:: https://img.shields.io/badge/Say%20Thanks-!-ff7efd.svg
|
||||||
|
:alt: Say Thanks!
|
||||||
|
:target: https://saythanks.io/to/aandergr
|
||||||
|
|
||||||
|
.. |aur| image:: https://img.shields.io/aur/version/instaloader.svg
|
||||||
|
:alt: Arch User Repository Package
|
||||||
|
:target: https://aur.archlinux.org/packages/instaloader/
|
||||||
|
|
||||||
|
.. |downloads| image:: https://img.shields.io/pypi/dm/instaloader.svg
|
||||||
|
:alt: PyPI Download Count
|
||||||
|
|
||||||
|
.. badges-end
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
$ pip3 install instaloader
|
$ pip3 install instaloader
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
pkgbase = instaloader
|
||||||
|
pkgdesc = Command line tool to download pictures, videos and metadata from Instagram
|
||||||
|
pkgver = {{version}}
|
||||||
|
pkgrel = 1
|
||||||
|
url = https://instaloader.github.io/
|
||||||
|
arch = any
|
||||||
|
license = MIT
|
||||||
|
makedepends = python-setuptools
|
||||||
|
depends = python>=3.5
|
||||||
|
depends = python-requests>=2.4
|
||||||
|
options = !emptydirs
|
||||||
|
source = instaloader-{{version}}.tar.gz::https://codeload.github.com/instaloader/instaloader/tar.gz/v{{version}}
|
||||||
|
sha512sums = {{hash}}
|
||||||
|
|
||||||
|
pkgname = instaloader
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Maintainer: André Koch-Kramer <koch-kramer@web.de>
|
||||||
|
|
||||||
|
pkgname=instaloader
|
||||||
|
pkgver={{version}}
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="Command line tool to download pictures, videos and metadata from Instagram"
|
||||||
|
arch=('any')
|
||||||
|
url="https://instaloader.github.io/"
|
||||||
|
license=('MIT')
|
||||||
|
groups=()
|
||||||
|
depends=('python>=3.5' 'python-requests>=2.4')
|
||||||
|
makedepends=('python-setuptools')
|
||||||
|
options=('!emptydirs')
|
||||||
|
source=($pkgname-$pkgver.tar.gz::https://codeload.github.com/instaloader/instaloader/tar.gz/v$pkgver)
|
||||||
|
sha512sums=('{{hash}}')
|
||||||
|
|
||||||
|
build() {
|
||||||
|
cd "$srcdir/$pkgname-$pkgver"
|
||||||
|
python setup.py clean --all
|
||||||
|
python setup.py build
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "$srcdir/$pkgname-$pkgver"
|
||||||
|
install -Dm644 LICENSE "$pkgdir"/usr/share/licenses/$pkgname/LICENSE
|
||||||
|
python setup.py install --root "$pkgdir/" --skip-build
|
||||||
|
}
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
VERSION=${1:1}
|
||||||
|
|
||||||
|
# do not deploy pre-releases
|
||||||
|
echo $VERSION | grep -qe "[abc]" && exit 0
|
||||||
|
|
||||||
|
cd $(dirname $0)
|
||||||
|
|
||||||
|
# decrypt and add ssh key
|
||||||
|
openssl aes-256-cbc -K $encrypted_354637631c28_key -iv $encrypted_354637631c28_iv -in id_rsa_AUR.enc -out /tmp/AUR_openssh -d
|
||||||
|
eval "$(ssh-agent -s)"
|
||||||
|
chmod 600 /tmp/AUR_openssh
|
||||||
|
ssh-add /tmp/AUR_openssh
|
||||||
|
|
||||||
|
# clone and modify AUR repo
|
||||||
|
git clone --depth 1 ssh://aur@aur.archlinux.org/instaloader.git
|
||||||
|
|
||||||
|
curl -sSfOJ https://codeload.github.com/instaloader/instaloader/tar.gz/$1
|
||||||
|
HASH=$(sha512sum instaloader-$VERSION.tar.gz | cut -f1 -d " ")
|
||||||
|
sed -e "s/{{version}}/$VERSION/g" -e "s/{{hash}}/$HASH/g" PKGBUILD.template > instaloader/PKGBUILD
|
||||||
|
sed -e "s/{{version}}/$VERSION/g" -e "s/{{hash}}/$HASH/g" .SRCINFO.template > instaloader/.SRCINFO
|
||||||
|
|
||||||
|
# commit and push changes
|
||||||
|
cd instaloader
|
||||||
|
git config user.email "koch-kramer@web.de"
|
||||||
|
git config user.name "André Koch-Kramer"
|
||||||
|
git add .
|
||||||
|
git commit -m "Release of version $VERSION"
|
||||||
|
git push
|
||||||
Binary file not shown.
@@ -123,6 +123,9 @@ Additionally, the following trivial structures are defined:
|
|||||||
.. autoclass:: PostComment
|
.. autoclass:: PostComment
|
||||||
:no-show-inheritance:
|
:no-show-inheritance:
|
||||||
|
|
||||||
|
.. autoclass:: PostCommentAnswer
|
||||||
|
:no-show-inheritance:
|
||||||
|
|
||||||
.. autoclass:: PostLocation
|
.. autoclass:: PostLocation
|
||||||
:no-show-inheritance:
|
:no-show-inheritance:
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ Instaloader
|
|||||||
**Instaloader** is a tool to download pictures (or videos) along with
|
**Instaloader** is a tool to download pictures (or videos) along with
|
||||||
their captions and other metadata from Instagram.
|
their captions and other metadata from Instagram.
|
||||||
|
|
||||||
|
.. include:: ../README.rst
|
||||||
|
:start-after: badges-start
|
||||||
|
:end-before: badges-end
|
||||||
|
|
||||||
With `Python <https://www.python.org/>`__ installed, do::
|
With `Python <https://www.python.org/>`__ installed, do::
|
||||||
|
|
||||||
$ pip3 install instaloader
|
$ pip3 install instaloader
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"""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.2.2'
|
__version__ = '4.2.8'
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# pylint:disable=wrong-import-position
|
# pylint:disable=wrong-import-position
|
||||||
import win_unicode_console
|
import win_unicode_console # type: ignore
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
@@ -15,5 +15,5 @@ else:
|
|||||||
from .exceptions import *
|
from .exceptions import *
|
||||||
from .instaloader import Instaloader
|
from .instaloader import Instaloader
|
||||||
from .instaloadercontext import InstaloaderContext
|
from .instaloadercontext import InstaloaderContext
|
||||||
from .structures import (Highlight, Post, PostSidecarNode, PostComment, PostLocation, Profile, Story, StoryItem,
|
from .structures import (Highlight, Post, PostSidecarNode, PostComment, PostCommentAnswer, PostLocation, Profile, Story,
|
||||||
load_structure_from_file, save_structure_to_file)
|
StoryItem, load_structure_from_file, save_structure_to_file)
|
||||||
|
|||||||
@@ -331,7 +331,6 @@ def main():
|
|||||||
g_how.add_argument('--user-agent',
|
g_how.add_argument('--user-agent',
|
||||||
help='User Agent to use for HTTP requests. Defaults to \'{}\'.'.format(default_user_agent()))
|
help='User Agent to use for HTTP requests. Defaults to \'{}\'.'.format(default_user_agent()))
|
||||||
g_how.add_argument('-S', '--no-sleep', action='store_true', help=SUPPRESS)
|
g_how.add_argument('-S', '--no-sleep', action='store_true', help=SUPPRESS)
|
||||||
g_how.add_argument('--graphql-rate-limit', type=int, help=SUPPRESS)
|
|
||||||
g_how.add_argument('--max-connection-attempts', metavar='N', type=int, default=3,
|
g_how.add_argument('--max-connection-attempts', metavar='N', type=int, default=3,
|
||||||
help='Maximum number of connection attempts until a request is aborted. Defaults to 3. If a '
|
help='Maximum number of connection attempts until a request is aborted. Defaults to 3. If a '
|
||||||
'connection fails, it can be manually skipped by hitting CTRL+C. Set this to 0 to retry '
|
'connection fails, it can be manually skipped by hitting CTRL+C. Set this to 0 to retry '
|
||||||
@@ -393,7 +392,6 @@ def main():
|
|||||||
compress_json=not args.no_compress_json,
|
compress_json=not args.no_compress_json,
|
||||||
post_metadata_txt_pattern=post_metadata_txt_pattern,
|
post_metadata_txt_pattern=post_metadata_txt_pattern,
|
||||||
storyitem_metadata_txt_pattern=storyitem_metadata_txt_pattern,
|
storyitem_metadata_txt_pattern=storyitem_metadata_txt_pattern,
|
||||||
graphql_rate_limit=args.graphql_rate_limit,
|
|
||||||
max_connection_attempts=args.max_connection_attempts,
|
max_connection_attempts=args.max_connection_attempts,
|
||||||
commit_mode=args.commit_mode)
|
commit_mode=args.commit_mode)
|
||||||
_main(loader,
|
_main(loader,
|
||||||
|
|||||||
+67
-39
@@ -13,14 +13,16 @@ from datetime import datetime, timezone
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Iterator, List, Optional, Set, Union
|
from typing import Any, Callable, Iterator, List, Optional, Set, Union
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import urllib3
|
import urllib3 # type: ignore
|
||||||
|
|
||||||
from .exceptions import *
|
from .exceptions import *
|
||||||
from .instaloadercontext import InstaloaderContext
|
from .instaloadercontext import InstaloaderContext
|
||||||
from .structures import Highlight, JsonExportable, Post, PostLocation, Profile, Story, StoryItem, save_structure_to_file, load_structure_from_file
|
from .structures import (Highlight, JsonExportable, Post, PostLocation, Profile, Story, StoryItem,
|
||||||
|
save_structure_to_file, load_structure_from_file)
|
||||||
|
|
||||||
|
|
||||||
def get_default_session_filename(username: str) -> str:
|
def get_default_session_filename(username: str) -> str:
|
||||||
@@ -45,8 +47,8 @@ def _requires_login(func: Callable) -> Callable:
|
|||||||
if not instaloader.context.is_logged_in:
|
if not instaloader.context.is_logged_in:
|
||||||
raise LoginRequiredException("--login=USERNAME required.")
|
raise LoginRequiredException("--login=USERNAME required.")
|
||||||
return func(instaloader, *args, **kwargs)
|
return func(instaloader, *args, **kwargs)
|
||||||
# pylint:disable=no-member
|
docstring_text = ":raises LoginRequiredException: If called without being logged in.\n"
|
||||||
call.__doc__ += ":raises LoginRequiredException: If called without being logged in.\n"
|
call.__doc__ = call.__doc__ + docstring_text if call.__doc__ is not None else docstring_text
|
||||||
return call
|
return call
|
||||||
|
|
||||||
|
|
||||||
@@ -108,7 +110,17 @@ class _PostPathFormatter(_ArbitraryItemFormatter):
|
|||||||
ret = super().vformat(format_string, args, kwargs)
|
ret = super().vformat(format_string, args, kwargs)
|
||||||
if platform.system() == 'Windows':
|
if platform.system() == 'Windows':
|
||||||
ret = ret.replace(':', '\ua789').replace('<', '\ufe64').replace('>', '\ufe65').replace('\"', '\uff02')
|
ret = ret.replace(':', '\ua789').replace('<', '\ufe64').replace('>', '\ufe65').replace('\"', '\uff02')
|
||||||
ret = ret.replace('\\', '\uff3c').replace('|', '\uff5c').replace('?', '\ufe16').replace('*', '\uff0a')
|
ret = ret.replace('\\', '\ufe68').replace('|', '\uff5c').replace('?', '\ufe16').replace('*', '\uff0a')
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def get_value(self, key, args, kwargs):
|
||||||
|
"""Replaces '/' with similar looking Division Slash and on windows newline with space"""
|
||||||
|
ret = super().get_value(key, args, kwargs)
|
||||||
|
if not isinstance(ret, str):
|
||||||
|
return ret
|
||||||
|
ret = ret.replace('/', '\u2215')
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
ret = ret.replace('\n', ' ')
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
@@ -152,11 +164,10 @@ class Instaloader:
|
|||||||
compress_json: bool = True,
|
compress_json: bool = True,
|
||||||
post_metadata_txt_pattern: str = None,
|
post_metadata_txt_pattern: str = None,
|
||||||
storyitem_metadata_txt_pattern: str = None,
|
storyitem_metadata_txt_pattern: str = None,
|
||||||
graphql_rate_limit: Optional[int] = None,
|
|
||||||
max_connection_attempts: int = 3,
|
max_connection_attempts: int = 3,
|
||||||
commit_mode: bool = False):
|
commit_mode: bool = False):
|
||||||
|
|
||||||
self.context = InstaloaderContext(sleep, quiet, user_agent, graphql_rate_limit, max_connection_attempts)
|
self.context = InstaloaderContext(sleep, quiet, user_agent, max_connection_attempts)
|
||||||
|
|
||||||
# configuration parameters
|
# configuration parameters
|
||||||
self.dirname_pattern = dirname_pattern or "{target}"
|
self.dirname_pattern = dirname_pattern or "{target}"
|
||||||
@@ -177,7 +188,7 @@ class Instaloader:
|
|||||||
raise InvalidArgumentException("Commit mode requires JSON metadata to be saved.")
|
raise InvalidArgumentException("Commit mode requires JSON metadata to be saved.")
|
||||||
|
|
||||||
# Used to keep state in commit mode
|
# Used to keep state in commit mode
|
||||||
self._committed = None
|
self._committed = None # type: Optional[bool]
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def anonymous_copy(self):
|
def anonymous_copy(self):
|
||||||
@@ -190,13 +201,10 @@ class Instaloader:
|
|||||||
save_metadata=self.save_metadata, compress_json=self.compress_json,
|
save_metadata=self.save_metadata, compress_json=self.compress_json,
|
||||||
post_metadata_txt_pattern=self.post_metadata_txt_pattern,
|
post_metadata_txt_pattern=self.post_metadata_txt_pattern,
|
||||||
storyitem_metadata_txt_pattern=self.storyitem_metadata_txt_pattern,
|
storyitem_metadata_txt_pattern=self.storyitem_metadata_txt_pattern,
|
||||||
graphql_rate_limit=self.context.graphql_count_per_slidingwindow,
|
|
||||||
max_connection_attempts=self.context.max_connection_attempts)
|
max_connection_attempts=self.context.max_connection_attempts)
|
||||||
new_loader.context.query_timestamps = self.context.query_timestamps
|
|
||||||
yield new_loader
|
yield new_loader
|
||||||
self.context.error_log.extend(new_loader.context.error_log)
|
self.context.error_log.extend(new_loader.context.error_log)
|
||||||
new_loader.context.error_log = [] # avoid double-printing of errors
|
new_loader.context.error_log = [] # avoid double-printing of errors
|
||||||
self.context.query_timestamps = new_loader.context.query_timestamps
|
|
||||||
new_loader.close()
|
new_loader.close()
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
@@ -244,11 +252,33 @@ class Instaloader:
|
|||||||
self.context.log('json', end=' ', flush=True)
|
self.context.log('json', end=' ', flush=True)
|
||||||
|
|
||||||
def update_comments(self, filename: str, post: Post) -> None:
|
def update_comments(self, filename: str, post: Post) -> None:
|
||||||
def _postcomment_asdict(comment):
|
def _postcommentanswer_asdict(comment):
|
||||||
return {'id': comment.id,
|
return {'id': comment.id,
|
||||||
'created_at': int(comment.created_at_utc.replace(tzinfo=timezone.utc).timestamp()),
|
'created_at': int(comment.created_at_utc.replace(tzinfo=timezone.utc).timestamp()),
|
||||||
'text': comment.text,
|
'text': comment.text,
|
||||||
'owner': comment.owner._asdict()}
|
'owner': comment.owner._asdict()}
|
||||||
|
|
||||||
|
def _postcomment_asdict(comment):
|
||||||
|
return {**_postcommentanswer_asdict(comment),
|
||||||
|
'answers': sorted([_postcommentanswer_asdict(answer) for answer in comment.answers],
|
||||||
|
key=lambda t: int(t['id']),
|
||||||
|
reverse=True)}
|
||||||
|
|
||||||
|
def get_unique_comments(comments, combine_answers=False):
|
||||||
|
if not comments:
|
||||||
|
return list()
|
||||||
|
comments_list = sorted(sorted(list(comments), key=lambda t: int(t['id'])),
|
||||||
|
key=lambda t: int(t['created_at']), reverse=True)
|
||||||
|
unique_comments_list = [comments_list[0]]
|
||||||
|
for x, y in zip(comments_list[:-1], comments_list[1:]):
|
||||||
|
if x['id'] != y['id']:
|
||||||
|
unique_comments_list.append(y)
|
||||||
|
elif combine_answers:
|
||||||
|
combined_answers = unique_comments_list[-1].get('answers') or list()
|
||||||
|
if 'answers' in y:
|
||||||
|
combined_answers.extend(y['answers'])
|
||||||
|
unique_comments_list[-1]['answers'] = get_unique_comments(combined_answers)
|
||||||
|
return unique_comments_list
|
||||||
filename += '_comments.json'
|
filename += '_comments.json'
|
||||||
try:
|
try:
|
||||||
with open(filename) as fp:
|
with open(filename) as fp:
|
||||||
@@ -257,18 +287,10 @@ class Instaloader:
|
|||||||
comments = list()
|
comments = list()
|
||||||
comments.extend(_postcomment_asdict(comment) for comment in post.get_comments())
|
comments.extend(_postcomment_asdict(comment) for comment in post.get_comments())
|
||||||
if comments:
|
if comments:
|
||||||
comments_list = sorted(sorted(list(comments), key=lambda t: int(t['id'])),
|
comments = get_unique_comments(comments, combine_answers=True)
|
||||||
key=lambda t: int(t['created_at']), reverse=True)
|
answer_ids = set(int(answer['id']) for comment in comments for answer in comment.get('answers', []))
|
||||||
unique_comments_list = [comments_list[0]]
|
|
||||||
#for comment in comments_list:
|
|
||||||
# if unique_comments_list[-1]['id'] != comment['id']:
|
|
||||||
# unique_comments_list.append(comment)
|
|
||||||
#file.write(json.dumps(unique_comments_list, indent=4))
|
|
||||||
for x, y in zip(comments_list[:-1], comments_list[1:]):
|
|
||||||
if x['id'] != y['id']:
|
|
||||||
unique_comments_list.append(y)
|
|
||||||
with open(filename, 'w') as file:
|
with open(filename, 'w') as file:
|
||||||
file.write(json.dumps(unique_comments_list, indent=4))
|
file.write(json.dumps(list(filter(lambda t: int(t['id']) not in answer_ids, comments)), indent=4))
|
||||||
self.context.log('comments', end=' ', flush=True)
|
self.context.log('comments', end=' ', flush=True)
|
||||||
|
|
||||||
def save_caption(self, filename: str, mtime: datetime, caption: str) -> None:
|
def save_caption(self, filename: str, mtime: datetime, caption: str) -> None:
|
||||||
@@ -279,11 +301,11 @@ class Instaloader:
|
|||||||
filename += '.txt'
|
filename += '.txt'
|
||||||
caption += '\n'
|
caption += '\n'
|
||||||
pcaption = _elliptify(caption)
|
pcaption = _elliptify(caption)
|
||||||
caption = caption.encode("UTF-8")
|
bcaption = caption.encode("UTF-8")
|
||||||
with suppress(FileNotFoundError):
|
with suppress(FileNotFoundError):
|
||||||
with open(filename, 'rb') as file:
|
with open(filename, 'rb') as file:
|
||||||
file_caption = file.read()
|
file_caption = file.read()
|
||||||
if file_caption.replace(b'\r\n', b'\n') == caption.replace(b'\r\n', b'\n'):
|
if file_caption.replace(b'\r\n', b'\n') == bcaption.replace(b'\r\n', b'\n'):
|
||||||
try:
|
try:
|
||||||
self.context.log(pcaption + ' unchanged', end=' ', flush=True)
|
self.context.log(pcaption + ' unchanged', end=' ', flush=True)
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
@@ -307,7 +329,7 @@ class Instaloader:
|
|||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
self.context.log('txt', end=' ', flush=True)
|
self.context.log('txt', end=' ', flush=True)
|
||||||
with open(filename, 'wb') as text_file:
|
with open(filename, 'wb') as text_file:
|
||||||
shutil.copyfileobj(BytesIO(caption), text_file)
|
shutil.copyfileobj(BytesIO(bcaption), text_file)
|
||||||
os.utime(filename, (datetime.now().timestamp(), mtime.timestamp()))
|
os.utime(filename, (datetime.now().timestamp(), mtime.timestamp()))
|
||||||
|
|
||||||
def save_location(self, filename: str, location: PostLocation, mtime: datetime) -> None:
|
def save_location(self, filename: str, location: PostLocation, mtime: datetime) -> None:
|
||||||
@@ -329,12 +351,12 @@ class Instaloader:
|
|||||||
return epoch.strftime('%Y-%m-%d_%H-%M-%S_UTC')
|
return epoch.strftime('%Y-%m-%d_%H-%M-%S_UTC')
|
||||||
|
|
||||||
profile_pic_response = self.context.get_raw(profile.profile_pic_url)
|
profile_pic_response = self.context.get_raw(profile.profile_pic_url)
|
||||||
|
date_object = None # type: Optional[datetime]
|
||||||
if 'Last-Modified' in profile_pic_response.headers:
|
if 'Last-Modified' in profile_pic_response.headers:
|
||||||
date_object = datetime.strptime(profile_pic_response.headers["Last-Modified"], '%a, %d %b %Y %H:%M:%S GMT')
|
date_object = datetime.strptime(profile_pic_response.headers["Last-Modified"], '%a, %d %b %Y %H:%M:%S GMT')
|
||||||
profile_pic_bytes = None
|
profile_pic_bytes = None
|
||||||
profile_pic_identifier = _epoch_to_string(date_object)
|
profile_pic_identifier = _epoch_to_string(date_object)
|
||||||
else:
|
else:
|
||||||
date_object = None
|
|
||||||
profile_pic_bytes = profile_pic_response.content
|
profile_pic_bytes = profile_pic_response.content
|
||||||
profile_pic_identifier = md5(profile_pic_bytes).hexdigest()[:16]
|
profile_pic_identifier = md5(profile_pic_bytes).hexdigest()[:16]
|
||||||
profile_pic_extension = 'jpg'
|
profile_pic_extension = 'jpg'
|
||||||
@@ -348,7 +370,8 @@ class Instaloader:
|
|||||||
profile_pic_identifier, profile_pic_extension)
|
profile_pic_identifier, profile_pic_extension)
|
||||||
content_length = profile_pic_response.headers.get('Content-Length', None)
|
content_length = profile_pic_response.headers.get('Content-Length', None)
|
||||||
if os.path.isfile(filename) and (not self.context.is_logged_in or
|
if os.path.isfile(filename) and (not self.context.is_logged_in or
|
||||||
content_length is not None and os.path.getsize(filename) >= int(content_length)):
|
(content_length is not None and
|
||||||
|
os.path.getsize(filename) >= int(content_length))):
|
||||||
self.context.log(filename + ' already exists')
|
self.context.log(filename + ' already exists')
|
||||||
return None
|
return None
|
||||||
self.context.write_raw(profile_pic_bytes if profile_pic_bytes else profile_pic_response, filename)
|
self.context.write_raw(profile_pic_bytes if profile_pic_bytes else profile_pic_response, filename)
|
||||||
@@ -363,6 +386,7 @@ class Instaloader:
|
|||||||
:param filename: Filename, or None to use default filename.
|
:param filename: Filename, or None to use default filename.
|
||||||
"""
|
"""
|
||||||
if filename is None:
|
if filename is None:
|
||||||
|
assert self.context.username is not None
|
||||||
filename = get_default_session_filename(self.context.username)
|
filename = get_default_session_filename(self.context.username)
|
||||||
dirname = os.path.dirname(filename)
|
dirname = os.path.dirname(filename)
|
||||||
if dirname != '' and not os.path.exists(dirname):
|
if dirname != '' and not os.path.exists(dirname):
|
||||||
@@ -396,7 +420,8 @@ class Instaloader:
|
|||||||
:raises InvalidArgumentException: If the provided username does not exist.
|
:raises InvalidArgumentException: If the provided username does not exist.
|
||||||
:raises BadCredentialsException: If the provided password is wrong.
|
:raises BadCredentialsException: If the provided password is wrong.
|
||||||
:raises ConnectionException: If connection to Instagram failed.
|
:raises ConnectionException: If connection to Instagram failed.
|
||||||
:raises TwoFactorAuthRequiredException: First step of 2FA login done, now call :meth:`Instaloader.two_factor_login`."""
|
:raises TwoFactorAuthRequiredException: First step of 2FA login done, now call
|
||||||
|
:meth:`Instaloader.two_factor_login`."""
|
||||||
self.context.login(user, passwd)
|
self.context.login(user, passwd)
|
||||||
|
|
||||||
def two_factor_login(self, two_factor_code) -> None:
|
def two_factor_login(self, two_factor_code) -> None:
|
||||||
@@ -409,13 +434,13 @@ class Instaloader:
|
|||||||
.. versionadded:: 4.2"""
|
.. versionadded:: 4.2"""
|
||||||
self.context.two_factor_login(two_factor_code)
|
self.context.two_factor_login(two_factor_code)
|
||||||
|
|
||||||
def format_filename(self, item: Union[Post, StoryItem], target: Optional[str] = None):
|
def format_filename(self, item: Union[Post, StoryItem], target: Optional[Union[str, Path]] = None):
|
||||||
"""Format filename of a :class:`Post` or :class:`StoryItem` according to ``filename-pattern`` parameter.
|
"""Format filename of a :class:`Post` or :class:`StoryItem` according to ``filename-pattern`` parameter.
|
||||||
|
|
||||||
.. versionadded:: 4.1"""
|
.. versionadded:: 4.1"""
|
||||||
return _PostPathFormatter(item).format(self.filename_pattern, target=target)
|
return _PostPathFormatter(item).format(self.filename_pattern, target=target)
|
||||||
|
|
||||||
def download_post(self, post: Post, target: str) -> bool:
|
def download_post(self, post: Post, target: Union[str, Path]) -> bool:
|
||||||
"""
|
"""
|
||||||
Download everything associated with one instagram post node, i.e. picture, caption and video.
|
Download everything associated with one instagram post node, i.e. picture, caption and video.
|
||||||
|
|
||||||
@@ -493,6 +518,7 @@ class Instaloader:
|
|||||||
userids = list(edge["node"]["id"] for edge in data["feed_reels_tray"]["edge_reels_tray_to_reel"]["edges"])
|
userids = list(edge["node"]["id"] for edge in data["feed_reels_tray"]["edge_reels_tray_to_reel"]["edges"])
|
||||||
|
|
||||||
def _userid_chunks():
|
def _userid_chunks():
|
||||||
|
assert userids is not None
|
||||||
userids_per_query = 100
|
userids_per_query = 100
|
||||||
for i in range(0, len(userids), userids_per_query):
|
for i in range(0, len(userids), userids_per_query):
|
||||||
yield userids[i:i + userids_per_query]
|
yield userids[i:i + userids_per_query]
|
||||||
@@ -541,7 +567,7 @@ class Instaloader:
|
|||||||
if fast_update and not downloaded:
|
if fast_update and not downloaded:
|
||||||
break
|
break
|
||||||
|
|
||||||
def download_storyitem(self, item: StoryItem, target: str) -> bool:
|
def download_storyitem(self, item: StoryItem, target: Union[str, Path]) -> bool:
|
||||||
"""Download one user story.
|
"""Download one user story.
|
||||||
|
|
||||||
:param item: Story item, as in story['items'] for story in :meth:`get_stories`
|
:param item: Story item, as in story['items'] for story in :meth:`get_stories`
|
||||||
@@ -618,10 +644,11 @@ class Instaloader:
|
|||||||
continue
|
continue
|
||||||
self.context.log("[%3i/%3i] " % (count, totalcount), end="", flush=True)
|
self.context.log("[%3i/%3i] " % (count, totalcount), end="", flush=True)
|
||||||
count += 1
|
count += 1
|
||||||
with self.context.error_catcher('Download highlights \"{}\" from user {}'.format(user_highlight.title, name)):
|
with self.context.error_catcher('Download highlights \"{}\" from user {}'.format(user_highlight.title,
|
||||||
|
name)):
|
||||||
downloaded = self.download_storyitem(item, filename_target
|
downloaded = self.download_storyitem(item, filename_target
|
||||||
if filename_target
|
if filename_target
|
||||||
else '{}/{}'.format(name, user_highlight.title))
|
else Path(name) / Path(user_highlight.title))
|
||||||
if fast_update and not downloaded:
|
if fast_update and not downloaded:
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -691,6 +718,7 @@ class Instaloader:
|
|||||||
"""
|
"""
|
||||||
self.context.log("Retrieving saved posts...")
|
self.context.log("Retrieving saved posts...")
|
||||||
count = 1
|
count = 1
|
||||||
|
assert self.context.username is not None
|
||||||
for post in Profile.from_username(self.context, self.context.username).get_saved_posts():
|
for post in Profile.from_username(self.context, self.context.username).get_saved_posts():
|
||||||
if max_count is not None and count > max_count:
|
if max_count is not None and count > max_count:
|
||||||
break
|
break
|
||||||
@@ -768,7 +796,7 @@ class Instaloader:
|
|||||||
for node in self.context.graphql_node_list("df0dcc250c2b18d9fd27c5581ef33c7c",
|
for node in self.context.graphql_node_list("df0dcc250c2b18d9fd27c5581ef33c7c",
|
||||||
{}, 'https://www.instagram.com/explore/',
|
{}, 'https://www.instagram.com/explore/',
|
||||||
lambda d: d['data']['user']['edge_web_discover_media'],
|
lambda d: d['data']['user']['edge_web_discover_media'],
|
||||||
data['rhx_gis']))
|
data.get('rhx_gis')))
|
||||||
|
|
||||||
def get_hashtag_posts(self, hashtag: str) -> Iterator[Post]:
|
def get_hashtag_posts(self, hashtag: str) -> Iterator[Post]:
|
||||||
"""Get Posts associated with a #hashtag."""
|
"""Get Posts associated with a #hashtag."""
|
||||||
@@ -823,8 +851,6 @@ class Instaloader:
|
|||||||
"""Download all posts where a profile is tagged.
|
"""Download all posts where a profile is tagged.
|
||||||
|
|
||||||
.. versionadded:: 4.1"""
|
.. versionadded:: 4.1"""
|
||||||
if target is None:
|
|
||||||
target = profile.username + '/:tagged'
|
|
||||||
self.context.log("Retrieving tagged posts for profile {}.".format(profile.username))
|
self.context.log("Retrieving tagged posts for profile {}.".format(profile.username))
|
||||||
count = 1
|
count = 1
|
||||||
for post in profile.get_tagged_posts():
|
for post in profile.get_tagged_posts():
|
||||||
@@ -833,7 +859,7 @@ class Instaloader:
|
|||||||
if post_filter is not None and not post_filter(post):
|
if post_filter is not None and not post_filter(post):
|
||||||
self.context.log('<{} skipped>'.format(post))
|
self.context.log('<{} skipped>'.format(post))
|
||||||
with self.context.error_catcher('Download tagged {}'.format(profile.username)):
|
with self.context.error_catcher('Download tagged {}'.format(profile.username)):
|
||||||
downloaded = self.download_post(post, target)
|
downloaded = self.download_post(post, target if target else Path(profile.username) / Path(':tagged'))
|
||||||
if fast_update and not downloaded:
|
if fast_update and not downloaded:
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -926,13 +952,15 @@ class Instaloader:
|
|||||||
|
|
||||||
.. versionadded:: 4.1"""
|
.. versionadded:: 4.1"""
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
def _error_raiser(_str):
|
def _error_raiser(_str):
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
# error_handler type is Callable[[Optional[str]], ContextManager[None]] (not supported with Python 3.5)
|
||||||
error_handler = _error_raiser if raise_errors else self.context.error_catcher
|
error_handler = _error_raiser if raise_errors else self.context.error_catcher
|
||||||
|
|
||||||
for profile in profiles:
|
for profile in profiles:
|
||||||
with error_handler(profile.username):
|
with error_handler(profile.username): # type: ignore
|
||||||
profile_name = profile.username
|
profile_name = profile.username
|
||||||
|
|
||||||
# Download profile picture
|
# Download profile picture
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import time
|
|||||||
import urllib.parse
|
import urllib.parse
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Callable, Dict, Iterator, Optional, Union
|
from typing import Any, Callable, Dict, Iterator, List, Optional, Union
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import requests.utils
|
import requests.utils
|
||||||
@@ -22,7 +22,7 @@ def copy_session(session: requests.Session) -> requests.Session:
|
|||||||
"""Duplicates a requests.Session."""
|
"""Duplicates a requests.Session."""
|
||||||
new = requests.Session()
|
new = requests.Session()
|
||||||
new.cookies = requests.utils.cookiejar_from_dict(requests.utils.dict_from_cookiejar(session.cookies))
|
new.cookies = requests.utils.cookiejar_from_dict(requests.utils.dict_from_cookiejar(session.cookies))
|
||||||
new.headers = session.headers.copy()
|
new.headers = session.headers.copy() # type: ignore
|
||||||
return new
|
return new
|
||||||
|
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ class InstaloaderContext:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, sleep: bool = True, quiet: bool = False, user_agent: Optional[str] = None,
|
def __init__(self, sleep: bool = True, quiet: bool = False, user_agent: Optional[str] = None,
|
||||||
graphql_count_per_slidingwindow: Optional[int] = None, max_connection_attempts: int = 3):
|
max_connection_attempts: int = 3):
|
||||||
|
|
||||||
self.user_agent = user_agent if user_agent is not None else default_user_agent()
|
self.user_agent = user_agent if user_agent is not None else default_user_agent()
|
||||||
self._session = self.get_anonymous_session()
|
self._session = self.get_anonymous_session()
|
||||||
@@ -56,21 +56,21 @@ class InstaloaderContext:
|
|||||||
self.quiet = quiet
|
self.quiet = quiet
|
||||||
self.max_connection_attempts = max_connection_attempts
|
self.max_connection_attempts = max_connection_attempts
|
||||||
self._graphql_page_length = 50
|
self._graphql_page_length = 50
|
||||||
self.graphql_count_per_slidingwindow = graphql_count_per_slidingwindow or 200
|
|
||||||
self._root_rhx_gis = None
|
self._root_rhx_gis = None
|
||||||
self.two_factor_auth_pending = None
|
self.two_factor_auth_pending = None
|
||||||
|
|
||||||
# error log, filled with error() and printed at the end of Instaloader.main()
|
# error log, filled with error() and printed at the end of Instaloader.main()
|
||||||
self.error_log = []
|
self.error_log = [] # type: List[str]
|
||||||
|
|
||||||
# For the adaption of sleep intervals (rate control)
|
# For the adaption of sleep intervals (rate control)
|
||||||
self.query_timestamps = list()
|
self._graphql_query_timestamps = dict() # type: Dict[str, List[float]]
|
||||||
|
self._graphql_earliest_next_request_time = 0.0
|
||||||
|
|
||||||
# Can be set to True for testing, disables supression of InstaloaderContext._error_catcher
|
# Can be set to True for testing, disables supression of InstaloaderContext._error_catcher
|
||||||
self.raise_all_errors = False
|
self.raise_all_errors = False
|
||||||
|
|
||||||
# Cache profile from id (mapping from id to Profile)
|
# Cache profile from id (mapping from id to Profile)
|
||||||
self.profile_id_cache = dict()
|
self.profile_id_cache = dict() # type: Dict[int, Any]
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def anonymous_copy(self):
|
def anonymous_copy(self):
|
||||||
@@ -181,7 +181,8 @@ class InstaloaderContext:
|
|||||||
:raises InvalidArgumentException: If the provided username does not exist.
|
:raises InvalidArgumentException: If the provided username does not exist.
|
||||||
:raises BadCredentialsException: If the provided password is wrong.
|
:raises BadCredentialsException: If the provided password is wrong.
|
||||||
:raises ConnectionException: If connection to Instagram failed.
|
:raises ConnectionException: If connection to Instagram failed.
|
||||||
:raises TwoFactorAuthRequiredException: First step of 2FA login done, now call :meth:`Instaloader.two_factor_login`."""
|
:raises TwoFactorAuthRequiredException: First step of 2FA login done, now call
|
||||||
|
:meth:`Instaloader.two_factor_login`."""
|
||||||
import http.client
|
import http.client
|
||||||
# pylint:disable=protected-access
|
# pylint:disable=protected-access
|
||||||
http.client._MAXHEADERS = 200
|
http.client._MAXHEADERS = 200
|
||||||
@@ -265,6 +266,72 @@ class InstaloaderContext:
|
|||||||
if self.sleep:
|
if self.sleep:
|
||||||
time.sleep(min(random.expovariate(0.7), 5.0))
|
time.sleep(min(random.expovariate(0.7), 5.0))
|
||||||
|
|
||||||
|
def _dump_query_timestamps(self, current_time: float):
|
||||||
|
"""Output the number of GraphQL queries grouped by their query_hash within the last time."""
|
||||||
|
windows = [10, 11, 15, 20, 30, 60]
|
||||||
|
print("GraphQL requests:", file=sys.stderr)
|
||||||
|
for query_hash, times in self._graphql_query_timestamps.items():
|
||||||
|
print(" {}".format(query_hash), file=sys.stderr)
|
||||||
|
for window in windows:
|
||||||
|
reqs_in_sliding_window = sum(t > current_time - window * 60 for t in times)
|
||||||
|
print(" last {} minutes: {} requests".format(window, reqs_in_sliding_window), file=sys.stderr)
|
||||||
|
|
||||||
|
def _graphql_request_count_per_sliding_window(self, query_hash: str) -> int:
|
||||||
|
"""Return how many GraphQL requests can be done within the sliding window."""
|
||||||
|
if self.is_logged_in:
|
||||||
|
max_reqs = {'1cb6ec562846122743b61e492c85999f': 20, '33ba35852cb50da46f5b5e889df7d159': 20, 'iphone': 100}
|
||||||
|
else:
|
||||||
|
max_reqs = {'1cb6ec562846122743b61e492c85999f': 200, '33ba35852cb50da46f5b5e889df7d159': 200}
|
||||||
|
return max_reqs.get(query_hash) or min(max_reqs.values())
|
||||||
|
|
||||||
|
def _graphql_query_waittime(self, query_hash: str, current_time: float, untracked_queries: bool = False) -> float:
|
||||||
|
"""Calculate time needed to wait before GraphQL query can be executed."""
|
||||||
|
sliding_window = 660
|
||||||
|
if query_hash not in self._graphql_query_timestamps:
|
||||||
|
self._graphql_query_timestamps[query_hash] = []
|
||||||
|
self._graphql_query_timestamps[query_hash] = list(filter(lambda t: t > current_time - 60 * 60,
|
||||||
|
self._graphql_query_timestamps[query_hash]))
|
||||||
|
reqs_in_sliding_window = list(filter(lambda t: t > current_time - sliding_window,
|
||||||
|
self._graphql_query_timestamps[query_hash]))
|
||||||
|
count_per_sliding_window = self._graphql_request_count_per_sliding_window(query_hash)
|
||||||
|
if len(reqs_in_sliding_window) < count_per_sliding_window and not untracked_queries:
|
||||||
|
return max(0, self._graphql_earliest_next_request_time - current_time)
|
||||||
|
next_request_time = min(reqs_in_sliding_window) + sliding_window + 6
|
||||||
|
if untracked_queries:
|
||||||
|
self._graphql_earliest_next_request_time = next_request_time
|
||||||
|
return round(max(next_request_time, self._graphql_earliest_next_request_time) - current_time)
|
||||||
|
|
||||||
|
def _ratecontrol_graphql_query(self, query_hash: str, untracked_queries: bool = False):
|
||||||
|
"""Called before a GraphQL query is made in order to stay within Instagram's rate limits.
|
||||||
|
|
||||||
|
:param query_hash: The query_hash parameter of the query.
|
||||||
|
:param untracked_queries: True, if 429 has been returned to apply 429 logic.
|
||||||
|
"""
|
||||||
|
if not untracked_queries:
|
||||||
|
waittime = self._graphql_query_waittime(query_hash, time.monotonic(), untracked_queries)
|
||||||
|
assert waittime >= 0
|
||||||
|
if waittime > 10:
|
||||||
|
self.log('\nToo many queries in the last time. Need to wait {} seconds, until {:%H:%M}.'
|
||||||
|
.format(waittime, datetime.now() + timedelta(seconds=waittime)))
|
||||||
|
time.sleep(waittime)
|
||||||
|
if query_hash not in self._graphql_query_timestamps:
|
||||||
|
self._graphql_query_timestamps[query_hash] = [time.monotonic()]
|
||||||
|
else:
|
||||||
|
self._graphql_query_timestamps[query_hash].append(time.monotonic())
|
||||||
|
else:
|
||||||
|
text_for_429 = ("HTTP error code 429 was returned because too many queries occured in the last time. "
|
||||||
|
"Please do not use Instagram in your browser or run multiple instances of Instaloader "
|
||||||
|
"in parallel.")
|
||||||
|
print(textwrap.fill(text_for_429), file=sys.stderr)
|
||||||
|
current_time = time.monotonic()
|
||||||
|
waittime = self._graphql_query_waittime(query_hash, current_time, untracked_queries)
|
||||||
|
assert waittime >= 0
|
||||||
|
if waittime > 10:
|
||||||
|
self.log('The request will be retried in {} seconds, at {:%H:%M}.'
|
||||||
|
.format(waittime, datetime.now() + timedelta(seconds=waittime)))
|
||||||
|
self._dump_query_timestamps(current_time)
|
||||||
|
time.sleep(waittime)
|
||||||
|
|
||||||
def get_json(self, path: str, params: Dict[str, Any], host: str = 'www.instagram.com',
|
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]:
|
session: Optional[requests.Session] = None, _attempt=1) -> Dict[str, Any]:
|
||||||
"""JSON request to Instagram.
|
"""JSON request to Instagram.
|
||||||
@@ -278,32 +345,15 @@ class InstaloaderContext:
|
|||||||
:raises QueryReturnedNotFoundException: When the server responds with a 404.
|
:raises QueryReturnedNotFoundException: When the server responds with a 404.
|
||||||
:raises ConnectionException: When query repeatedly failed.
|
:raises ConnectionException: When query repeatedly failed.
|
||||||
"""
|
"""
|
||||||
def graphql_query_waittime(untracked_queries: bool = False) -> int:
|
|
||||||
sliding_window = 660
|
|
||||||
if not self.query_timestamps:
|
|
||||||
return sliding_window if untracked_queries else 0
|
|
||||||
current_time = time.monotonic()
|
|
||||||
self.query_timestamps = list(filter(lambda t: t > current_time - sliding_window, self.query_timestamps))
|
|
||||||
if len(self.query_timestamps) < self.graphql_count_per_slidingwindow and not untracked_queries:
|
|
||||||
return 0
|
|
||||||
return round(min(self.query_timestamps) + sliding_window - current_time) + 6
|
|
||||||
is_graphql_query = 'query_hash' in params and 'graphql/query' in path
|
is_graphql_query = 'query_hash' in params and 'graphql/query' in path
|
||||||
# some queries are not rate limited if invoked anonymously:
|
is_iphone_query = host == 'i.instagram.com'
|
||||||
query_not_limited = is_graphql_query and not self.is_logged_in \
|
|
||||||
and params['query_hash'] in ['9ca88e465c3f866a76f7adee3871bdd8']
|
|
||||||
if is_graphql_query and not query_not_limited:
|
|
||||||
waittime = graphql_query_waittime()
|
|
||||||
if waittime > 0:
|
|
||||||
self.log('\nToo many queries in the last time. Need to wait {} seconds, until {:%H:%M}.'
|
|
||||||
.format(waittime, datetime.now() + timedelta(seconds=waittime)))
|
|
||||||
time.sleep(waittime)
|
|
||||||
if self.query_timestamps is not None:
|
|
||||||
self.query_timestamps.append(time.monotonic())
|
|
||||||
else:
|
|
||||||
self.query_timestamps = [time.monotonic()]
|
|
||||||
sess = session if session else self._session
|
sess = session if session else self._session
|
||||||
try:
|
try:
|
||||||
self.do_sleep()
|
self.do_sleep()
|
||||||
|
if is_graphql_query:
|
||||||
|
self._ratecontrol_graphql_query(params['query_hash'])
|
||||||
|
if is_iphone_query:
|
||||||
|
self._ratecontrol_graphql_query('iphone')
|
||||||
resp = sess.get('https://{0}/{1}'.format(host, path), params=params, allow_redirects=False)
|
resp = sess.get('https://{0}/{1}'.format(host, path), params=params, allow_redirects=False)
|
||||||
while resp.is_redirect:
|
while resp.is_redirect:
|
||||||
redirect_url = resp.headers['location']
|
redirect_url = resp.headers['location']
|
||||||
@@ -341,20 +391,11 @@ class InstaloaderContext:
|
|||||||
if _attempt == self.max_connection_attempts:
|
if _attempt == self.max_connection_attempts:
|
||||||
raise ConnectionException(error_string) from err
|
raise ConnectionException(error_string) from err
|
||||||
self.error(error_string + " [retrying; skip with ^C]", repeat_at_end=False)
|
self.error(error_string + " [retrying; skip with ^C]", repeat_at_end=False)
|
||||||
text_for_429 = ("HTTP error code 429 was returned because too many queries occured in the last time. "
|
|
||||||
"Please do not use Instagram in your browser or run multiple instances of Instaloader "
|
|
||||||
"in parallel.")
|
|
||||||
try:
|
try:
|
||||||
if isinstance(err, TooManyRequestsException):
|
if is_graphql_query and isinstance(err, TooManyRequestsException):
|
||||||
print(textwrap.fill(text_for_429), file=sys.stderr)
|
self._ratecontrol_graphql_query(params['query_hash'], untracked_queries=True)
|
||||||
if is_graphql_query:
|
if is_iphone_query and isinstance(err, TooManyRequestsException):
|
||||||
print("Made {} GraphQL requests.".format(len(self.query_timestamps)), file=sys.stderr)
|
self._ratecontrol_graphql_query('iphone', untracked_queries=True)
|
||||||
waittime = graphql_query_waittime(untracked_queries=True)
|
|
||||||
if waittime > 0:
|
|
||||||
self.log('The request will be retried in {} seconds, at {:%H:%M}.'
|
|
||||||
.format(waittime, datetime.now() + timedelta(seconds=waittime)))
|
|
||||||
time.sleep(waittime)
|
|
||||||
self.do_sleep()
|
|
||||||
return self.get_json(path=path, params=params, host=host, session=sess, _attempt=_attempt + 1)
|
return self.get_json(path=path, params=params, host=host, session=sess, _attempt=_attempt + 1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
self.error("[skipped by user]", repeat_at_end=False)
|
self.error("[skipped by user]", repeat_at_end=False)
|
||||||
@@ -494,6 +535,6 @@ class InstaloaderContext:
|
|||||||
# At the moment, rhx_gis seems to be required for anonymous requests only. By returning None when logged
|
# At the moment, rhx_gis seems to be required for anonymous requests only. By returning None when logged
|
||||||
# in, we can save the root_rhx_gis lookup query.
|
# in, we can save the root_rhx_gis lookup query.
|
||||||
return None
|
return None
|
||||||
if not self._root_rhx_gis:
|
if self._root_rhx_gis is None:
|
||||||
self._root_rhx_gis = self.get_json('', {})['rhx_gis']
|
self._root_rhx_gis = self.get_json('', {}).get('rhx_gis', '')
|
||||||
return self._root_rhx_gis
|
return self._root_rhx_gis or None
|
||||||
|
|||||||
+122
-35
@@ -17,11 +17,16 @@ PostSidecarNode.is_video.__doc__ = "Whether this node is a video."
|
|||||||
PostSidecarNode.display_url.__doc__ = "URL of image or video thumbnail."
|
PostSidecarNode.display_url.__doc__ = "URL of image or video thumbnail."
|
||||||
PostSidecarNode.video_url.__doc__ = "URL of video or None."
|
PostSidecarNode.video_url.__doc__ = "URL of video or None."
|
||||||
|
|
||||||
PostComment = namedtuple('PostComment', ['id', 'created_at_utc', 'text', 'owner'])
|
PostCommentAnswer = namedtuple('PostCommentAnswer', ['id', 'created_at_utc', 'text', 'owner'])
|
||||||
PostComment.id.__doc__ = "ID number of comment."
|
PostCommentAnswer.id.__doc__ = "ID number of comment."
|
||||||
PostComment.created_at_utc.__doc__ = ":class:`~datetime.datetime` when comment was created (UTC)."
|
PostCommentAnswer.created_at_utc.__doc__ = ":class:`~datetime.datetime` when comment was created (UTC)."
|
||||||
PostComment.text.__doc__ = "Comment text."
|
PostCommentAnswer.text.__doc__ = "Comment text."
|
||||||
PostComment.owner.__doc__ = "Owner :class:`Profile` of the comment."
|
PostCommentAnswer.owner.__doc__ = "Owner :class:`Profile` of the comment."
|
||||||
|
|
||||||
|
PostComment = namedtuple('PostComment', (*PostCommentAnswer._fields, 'answers')) # type: ignore
|
||||||
|
for field in PostCommentAnswer._fields:
|
||||||
|
getattr(PostComment, field).__doc__ = getattr(PostCommentAnswer, field).__doc__
|
||||||
|
PostComment.answers.__doc__ = r"Iterator which yields all :class:`PostCommentAnswer`\ s for the comment." # type: ignore
|
||||||
|
|
||||||
PostLocation = namedtuple('PostLocation', ['id', 'name', 'slug', 'has_public_page', 'lat', 'lng'])
|
PostLocation = namedtuple('PostLocation', ['id', 'name', 'slug', 'has_public_page', 'lat', 'lng'])
|
||||||
PostLocation.id.__doc__ = "ID number of location."
|
PostLocation.id.__doc__ = "ID number of location."
|
||||||
@@ -62,9 +67,9 @@ class Post:
|
|||||||
self._context = context
|
self._context = context
|
||||||
self._node = node
|
self._node = node
|
||||||
self._owner_profile = owner_profile
|
self._owner_profile = owner_profile
|
||||||
self._full_metadata_dict = None
|
self._full_metadata_dict = None # type: Optional[Dict[str, Any]]
|
||||||
self._rhx_gis_str = None
|
self._rhx_gis_str = None # type: Optional[str]
|
||||||
self._location = None
|
self._location = None # type: Optional[PostLocation]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_shortcode(cls, context: InstaloaderContext, shortcode: str):
|
def from_shortcode(cls, context: InstaloaderContext, shortcode: str):
|
||||||
@@ -127,7 +132,7 @@ class Post:
|
|||||||
if not self._full_metadata_dict:
|
if not self._full_metadata_dict:
|
||||||
pic_json = self._context.get_json("p/{0}/".format(self.shortcode), params={})
|
pic_json = self._context.get_json("p/{0}/".format(self.shortcode), params={})
|
||||||
self._full_metadata_dict = pic_json['entry_data']['PostPage'][0]['graphql']['shortcode_media']
|
self._full_metadata_dict = pic_json['entry_data']['PostPage'][0]['graphql']['shortcode_media']
|
||||||
self._rhx_gis_str = pic_json['rhx_gis']
|
self._rhx_gis_str = pic_json.get('rhx_gis')
|
||||||
if self.shortcode != self._full_metadata_dict['shortcode']:
|
if self.shortcode != self._full_metadata_dict['shortcode']:
|
||||||
self._node.update(self._full_metadata_dict)
|
self._node.update(self._full_metadata_dict)
|
||||||
raise PostChangedException
|
raise PostChangedException
|
||||||
@@ -135,10 +140,11 @@ class Post:
|
|||||||
@property
|
@property
|
||||||
def _full_metadata(self) -> Dict[str, Any]:
|
def _full_metadata(self) -> Dict[str, Any]:
|
||||||
self._obtain_metadata()
|
self._obtain_metadata()
|
||||||
|
assert self._full_metadata_dict is not None
|
||||||
return self._full_metadata_dict
|
return self._full_metadata_dict
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _rhx_gis(self) -> str:
|
def _rhx_gis(self) -> Optional[str]:
|
||||||
self._obtain_metadata()
|
self._obtain_metadata()
|
||||||
return self._rhx_gis_str
|
return self._rhx_gis_str
|
||||||
|
|
||||||
@@ -183,12 +189,16 @@ class Post:
|
|||||||
@property
|
@property
|
||||||
def date_local(self) -> datetime:
|
def date_local(self) -> datetime:
|
||||||
"""Timestamp when the post was created (local time zone)."""
|
"""Timestamp when the post was created (local time zone)."""
|
||||||
return datetime.fromtimestamp(self._node["date"] if "date" in self._node else self._node["taken_at_timestamp"])
|
return datetime.fromtimestamp(self._node["date"]
|
||||||
|
if "date" in self._node
|
||||||
|
else self._node["taken_at_timestamp"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date_utc(self) -> datetime:
|
def date_utc(self) -> datetime:
|
||||||
"""Timestamp when the post was created (UTC)."""
|
"""Timestamp when the post was created (UTC)."""
|
||||||
return datetime.utcfromtimestamp(self._node["date"] if "date" in self._node else self._node["taken_at_timestamp"])
|
return datetime.utcfromtimestamp(self._node["date"]
|
||||||
|
if "date" in self._node
|
||||||
|
else self._node["taken_at_timestamp"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def date(self) -> datetime:
|
def date(self) -> datetime:
|
||||||
@@ -226,6 +236,7 @@ class Post:
|
|||||||
return self._node["edge_media_to_caption"]["edges"][0]["node"]["text"]
|
return self._node["edge_media_to_caption"]["edges"][0]["node"]["text"]
|
||||||
elif "caption" in self._node:
|
elif "caption" in self._node:
|
||||||
return self._node["caption"]
|
return self._node["caption"]
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def caption_hashtags(self) -> List[str]:
|
def caption_hashtags(self) -> List[str]:
|
||||||
@@ -247,6 +258,16 @@ class Post:
|
|||||||
mention_regex = re.compile(r"(?:@)(\w(?:(?:\w|(?:\.(?!\.))){0,28}(?:\w))?)")
|
mention_regex = re.compile(r"(?:@)(\w(?:(?:\w|(?:\.(?!\.))){0,28}(?:\w))?)")
|
||||||
return re.findall(mention_regex, self.caption.lower())
|
return re.findall(mention_regex, self.caption.lower())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pcaption(self) -> str:
|
||||||
|
"""Printable caption, useful as a format specifier for --filename-pattern.
|
||||||
|
|
||||||
|
.. versionadded:: 4.2.6"""
|
||||||
|
def _elliptify(caption):
|
||||||
|
pcaption = ' '.join([s.replace('/', '\u2215') for s in caption.splitlines() if s]).strip()
|
||||||
|
return (pcaption[:30] + u"\u2026") if len(pcaption) > 31 else pcaption
|
||||||
|
return _elliptify(self.caption) if self.caption else ''
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tagged_users(self) -> List[str]:
|
def tagged_users(self) -> List[str]:
|
||||||
"""List of all lowercased users that are tagged in the Post."""
|
"""List of all lowercased users that are tagged in the Post."""
|
||||||
@@ -266,6 +287,25 @@ class Post:
|
|||||||
"""URL of the video, or None."""
|
"""URL of the video, or None."""
|
||||||
if self.is_video:
|
if self.is_video:
|
||||||
return self._field('video_url')
|
return self._field('video_url')
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def video_view_count(self) -> Optional[int]:
|
||||||
|
"""View count of the video, or None.
|
||||||
|
|
||||||
|
.. versionadded:: 4.2.6"""
|
||||||
|
if self.is_video:
|
||||||
|
return self._field('video_view_count')
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def video_duration(self) -> Optional[float]:
|
||||||
|
"""Duration of the video in seconds, or None.
|
||||||
|
|
||||||
|
.. versionadded:: 4.2.6"""
|
||||||
|
if self.is_video:
|
||||||
|
return self._field('video_duration')
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def viewer_has_liked(self) -> Optional[bool]:
|
def viewer_has_liked(self) -> Optional[bool]:
|
||||||
@@ -283,34 +323,72 @@ class Post:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def comments(self) -> int:
|
def comments(self) -> int:
|
||||||
"""Comment count"""
|
"""Comment count including answers"""
|
||||||
return self._field('edge_media_to_comment', 'count')
|
try:
|
||||||
|
return self._field('edge_media_to_parent_comment', 'count')
|
||||||
|
except KeyError:
|
||||||
|
return self._field('edge_media_to_comment', 'count')
|
||||||
|
|
||||||
def get_comments(self) -> Iterator[PostComment]:
|
def get_comments(self) -> Iterator[PostComment]:
|
||||||
"""Iterate over all comments of the post.
|
r"""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) and owner (:class:`Profile`).
|
id (int), owner (:class:`Profile`) and answers (:class:`~typing.Iterator`\ [:class:`PostCommentAnswer`])
|
||||||
|
if available.
|
||||||
"""
|
"""
|
||||||
|
def _postcommentanswer(node):
|
||||||
|
return PostCommentAnswer(id=int(node['id']),
|
||||||
|
created_at_utc=datetime.utcfromtimestamp(node['created_at']),
|
||||||
|
text=node['text'],
|
||||||
|
owner=Profile(self._context, node['owner']))
|
||||||
|
|
||||||
|
def _postcommentanswers(node):
|
||||||
|
if 'edge_threaded_comments' not in node:
|
||||||
|
return
|
||||||
|
answer_count = node['edge_threaded_comments']['count']
|
||||||
|
if answer_count == 0:
|
||||||
|
# Avoid doing additional requests if there are no comment answers
|
||||||
|
return
|
||||||
|
answer_edges = node['edge_threaded_comments']['edges']
|
||||||
|
if answer_count == len(answer_edges):
|
||||||
|
# If the answer's metadata already contains all comments, don't do GraphQL requests to obtain them
|
||||||
|
yield from (_postcommentanswer(comment['node']) for comment in answer_edges)
|
||||||
|
return
|
||||||
|
yield from (_postcommentanswer(answer_node) for answer_node in
|
||||||
|
self._context.graphql_node_list("51fdd02b67508306ad4484ff574a0b62",
|
||||||
|
{'comment_id': node['id']},
|
||||||
|
'https://www.instagram.com/p/' + self.shortcode + '/',
|
||||||
|
lambda d: d['data']['comment']['edge_threaded_comments']))
|
||||||
|
|
||||||
def _postcomment(node):
|
def _postcomment(node):
|
||||||
return PostComment(id=int(node['id']),
|
return PostComment(*_postcommentanswer(node),
|
||||||
created_at_utc=datetime.utcfromtimestamp(node['created_at']),
|
answers=_postcommentanswers(node))
|
||||||
text=node['text'],
|
|
||||||
owner=Profile(self._context, node['owner']))
|
|
||||||
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
|
||||||
comment_edges = self._field('edge_media_to_comment', 'edges')
|
try:
|
||||||
if self.comments == len(comment_edges):
|
comment_edges = self._field('edge_media_to_parent_comment', 'edges')
|
||||||
# If the Post's metadata already contains all comments, don't do GraphQL requests to obtain them
|
answers_count = sum([edge['node']['edge_threaded_comments']['count'] for edge in comment_edges])
|
||||||
|
threaded_comments_available = True
|
||||||
|
except KeyError:
|
||||||
|
comment_edges = self._field('edge_media_to_comment', 'edges')
|
||||||
|
answers_count = 0
|
||||||
|
threaded_comments_available = False
|
||||||
|
|
||||||
|
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
|
||||||
yield from (_postcomment(comment['node']) for comment in comment_edges)
|
yield from (_postcomment(comment['node']) for comment in comment_edges)
|
||||||
return
|
return
|
||||||
yield from (_postcomment(node) for node in
|
yield from (_postcomment(node) for node in
|
||||||
self._context.graphql_node_list("33ba35852cb50da46f5b5e889df7d159",
|
self._context.graphql_node_list(
|
||||||
{'shortcode': self.shortcode},
|
"97b41c52301f77ce508f55e66d17620e" if threaded_comments_available
|
||||||
'https://www.instagram.com/p/' + self.shortcode + '/',
|
else "f0986789a5c5d17c2400faebf16efd0d",
|
||||||
lambda d: d['data']['shortcode_media']['edge_media_to_comment'],
|
{'shortcode': self.shortcode},
|
||||||
self._rhx_gis))
|
'https://www.instagram.com/p/' + self.shortcode + '/',
|
||||||
|
lambda d:
|
||||||
|
d['data']['shortcode_media'][
|
||||||
|
'edge_media_to_parent_comment' if threaded_comments_available else 'edge_media_to_comment'],
|
||||||
|
self._rhx_gis))
|
||||||
|
|
||||||
def get_likes(self) -> Iterator['Profile']:
|
def get_likes(self) -> Iterator['Profile']:
|
||||||
"""Iterate over all likes of the post. A :class:`Profile` instance of each likee is yielded."""
|
"""Iterate over all likes of the post. A :class:`Profile` instance of each likee is yielded."""
|
||||||
@@ -373,8 +451,9 @@ class Profile:
|
|||||||
def __init__(self, context: InstaloaderContext, node: Dict[str, Any]):
|
def __init__(self, context: InstaloaderContext, node: Dict[str, Any]):
|
||||||
assert 'username' in node
|
assert 'username' in node
|
||||||
self._context = context
|
self._context = context
|
||||||
self._has_public_story = None
|
self._has_public_story = None # type: Optional[bool]
|
||||||
self._node = node
|
self._node = node
|
||||||
|
self._has_full_metadata = False
|
||||||
self._rhx_gis = None
|
self._rhx_gis = None
|
||||||
self._iphone_struct_ = None
|
self._iphone_struct_ = None
|
||||||
if 'iphone_struct' in node:
|
if 'iphone_struct' in node:
|
||||||
@@ -435,10 +514,11 @@ class Profile:
|
|||||||
|
|
||||||
def _obtain_metadata(self):
|
def _obtain_metadata(self):
|
||||||
try:
|
try:
|
||||||
if not self._rhx_gis:
|
if not self._has_full_metadata:
|
||||||
metadata = self._context.get_json('{}/'.format(self.username), params={})
|
metadata = self._context.get_json('{}/'.format(self.username), params={})
|
||||||
self._node = metadata['entry_data']['ProfilePage'][0]['graphql']['user']
|
self._node = metadata['entry_data']['ProfilePage'][0]['graphql']['user']
|
||||||
self._rhx_gis = metadata['rhx_gis']
|
self._has_full_metadata = True
|
||||||
|
self._rhx_gis = metadata.get('rhx_gis')
|
||||||
except (QueryReturnedNotFoundException, KeyError) as err:
|
except (QueryReturnedNotFoundException, KeyError) as err:
|
||||||
raise ProfileNotExistsException('Profile {} does not exist.'.format(self.username)) from err
|
raise ProfileNotExistsException('Profile {} does not exist.'.format(self.username)) from err
|
||||||
|
|
||||||
@@ -553,6 +633,7 @@ class Profile:
|
|||||||
'https://www.instagram.com/{}/'.format(self.username),
|
'https://www.instagram.com/{}/'.format(self.username),
|
||||||
self._rhx_gis)
|
self._rhx_gis)
|
||||||
self._has_public_story = data['data']['user']['has_public_story']
|
self._has_public_story = data['data']['user']['has_public_story']
|
||||||
|
assert self._has_public_story is not None
|
||||||
return self._has_public_story
|
return self._has_public_story
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -719,6 +800,7 @@ class StoryItem:
|
|||||||
""":class:`Profile` instance of the story item's owner."""
|
""":class:`Profile` instance of the story item's owner."""
|
||||||
if not self._owner_profile:
|
if not self._owner_profile:
|
||||||
self._owner_profile = Profile.from_id(self._context, self._node['owner']['id'])
|
self._owner_profile = Profile.from_id(self._context, self._node['owner']['id'])
|
||||||
|
assert self._owner_profile is not None
|
||||||
return self._owner_profile
|
return self._owner_profile
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -781,6 +863,7 @@ class StoryItem:
|
|||||||
"""URL of the video, or None."""
|
"""URL of the video, or None."""
|
||||||
if self.is_video:
|
if self.is_video:
|
||||||
return self._node['video_resources'][-1]['src']
|
return self._node['video_resources'][-1]['src']
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class Story:
|
class Story:
|
||||||
@@ -807,8 +890,8 @@ class Story:
|
|||||||
def __init__(self, context: InstaloaderContext, node: Dict[str, Any]):
|
def __init__(self, context: InstaloaderContext, node: Dict[str, Any]):
|
||||||
self._context = context
|
self._context = context
|
||||||
self._node = node
|
self._node = node
|
||||||
self._unique_id = None
|
self._unique_id = None # type: Optional[str]
|
||||||
self._owner_profile = None
|
self._owner_profile = None # type: Optional[Profile]
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Story by {} changed {:%Y-%m-%d_%H-%M-%S_UTC}>'.format(self.owner_username, self.latest_media_utc)
|
return '<Story by {} changed {:%Y-%m-%d_%H-%M-%S_UTC}>'.format(self.owner_username, self.latest_media_utc)
|
||||||
@@ -822,7 +905,7 @@ class Story:
|
|||||||
return hash(self.unique_id)
|
return hash(self.unique_id)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self) -> str:
|
def unique_id(self) -> Union[str, int]:
|
||||||
"""
|
"""
|
||||||
This ID only equals amongst :class:`Story` instances which have the same owner and the same set of
|
This ID only equals amongst :class:`Story` instances which have the same owner and the same set of
|
||||||
:class:`StoryItem`. For all other :class:`Story` instances this ID is different.
|
:class:`StoryItem`. For all other :class:`Story` instances this ID is different.
|
||||||
@@ -838,12 +921,14 @@ class Story:
|
|||||||
"""Timestamp when the story has last been watched or None (local time zone)."""
|
"""Timestamp when the story has last been watched or None (local time zone)."""
|
||||||
if self._node['seen']:
|
if self._node['seen']:
|
||||||
return datetime.fromtimestamp(self._node['seen'])
|
return datetime.fromtimestamp(self._node['seen'])
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def last_seen_utc(self) -> Optional[datetime]:
|
def last_seen_utc(self) -> Optional[datetime]:
|
||||||
"""Timestamp when the story has last been watched or None (UTC)."""
|
"""Timestamp when the story has last been watched or None (UTC)."""
|
||||||
if self._node['seen']:
|
if self._node['seen']:
|
||||||
return datetime.utcfromtimestamp(self._node['seen'])
|
return datetime.utcfromtimestamp(self._node['seen'])
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def latest_media_local(self) -> datetime:
|
def latest_media_local(self) -> datetime:
|
||||||
@@ -908,7 +993,7 @@ class Highlight(Story):
|
|||||||
def __init__(self, context: InstaloaderContext, node: Dict[str, Any], owner: Optional[Profile] = None):
|
def __init__(self, context: InstaloaderContext, node: Dict[str, Any], owner: Optional[Profile] = None):
|
||||||
super().__init__(context, node)
|
super().__init__(context, node)
|
||||||
self._owner_profile = owner
|
self._owner_profile = owner
|
||||||
self._items = None
|
self._items = None # type: Optional[List[Dict[str, Any]]]
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<Highlight by {}: {}>'.format(self.owner_username, self.title)
|
return '<Highlight by {}: {}>'.format(self.owner_username, self.title)
|
||||||
@@ -951,11 +1036,13 @@ class Highlight(Story):
|
|||||||
def itemcount(self) -> int:
|
def itemcount(self) -> int:
|
||||||
"""Count of items associated with the :class:`Highlight` instance."""
|
"""Count of items associated with the :class:`Highlight` instance."""
|
||||||
self._fetch_items()
|
self._fetch_items()
|
||||||
|
assert self._items is not None
|
||||||
return len(self._items)
|
return len(self._items)
|
||||||
|
|
||||||
def get_items(self) -> Iterator[StoryItem]:
|
def get_items(self) -> Iterator[StoryItem]:
|
||||||
"""Retrieve all associated highlight items."""
|
"""Retrieve all associated highlight items."""
|
||||||
self._fetch_items()
|
self._fetch_items()
|
||||||
|
assert self._items is not None
|
||||||
yield from (StoryItem(self._context, item, self.owner_profile) for item in self._items)
|
yield from (StoryItem(self._context, item, self.owner_profile) for item in self._items)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ setup(
|
|||||||
'Programming Language :: Python :: 3.5',
|
'Programming Language :: Python :: 3.5',
|
||||||
'Programming Language :: Python :: 3.6',
|
'Programming Language :: Python :: 3.6',
|
||||||
'Programming Language :: Python :: 3.7',
|
'Programming Language :: Python :: 3.7',
|
||||||
|
'Programming Language :: Python :: 3.8',
|
||||||
'Programming Language :: Python :: 3 :: Only',
|
'Programming Language :: Python :: 3 :: Only',
|
||||||
'Topic :: Internet',
|
'Topic :: Internet',
|
||||||
'Topic :: Multimedia :: Graphics'
|
'Topic :: Multimedia :: Graphics'
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import shutil
|
|||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from itertools import islice
|
from itertools import islice
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
import instaloader
|
import instaloader
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ EMPTY_PROFILE = "not_public"
|
|||||||
EMPTY_PROFILE_ID = 1928659031
|
EMPTY_PROFILE_ID = 1928659031
|
||||||
|
|
||||||
# Preserve query timestamps (rate control) between tests to not get rate limited
|
# Preserve query timestamps (rate control) between tests to not get rate limited
|
||||||
instaloadercontext_query_timestamps = list()
|
instaloadercontext_query_timestamps = dict() # type: Dict[str, List[float]]
|
||||||
|
|
||||||
|
|
||||||
class TestInstaloaderAnonymously(unittest.TestCase):
|
class TestInstaloaderAnonymously(unittest.TestCase):
|
||||||
@@ -35,11 +36,13 @@ class TestInstaloaderAnonymously(unittest.TestCase):
|
|||||||
download_comments=True,
|
download_comments=True,
|
||||||
save_metadata=True)
|
save_metadata=True)
|
||||||
self.L.context.raise_all_errors = True
|
self.L.context.raise_all_errors = True
|
||||||
self.L.context.query_timestamps = instaloadercontext_query_timestamps
|
# pylint:disable=protected-access
|
||||||
|
self.L.context._graphql_query_timestamps = instaloadercontext_query_timestamps.copy()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
|
# pylint:disable=global-statement,protected-access
|
||||||
global instaloadercontext_query_timestamps
|
global instaloadercontext_query_timestamps
|
||||||
instaloadercontext_query_timestamps = self.L.context.query_timestamps
|
instaloadercontext_query_timestamps = self.L.context._graphql_query_timestamps.copy()
|
||||||
self.L.close()
|
self.L.close()
|
||||||
os.chdir('/')
|
os.chdir('/')
|
||||||
print("Removing {}".format(self.dir))
|
print("Removing {}".format(self.dir))
|
||||||
@@ -53,16 +56,17 @@ class TestInstaloaderAnonymously(unittest.TestCase):
|
|||||||
self.assertTrue(post.date_utc < previous_post.date_utc)
|
self.assertTrue(post.date_utc < previous_post.date_utc)
|
||||||
previous_post = post
|
previous_post = post
|
||||||
|
|
||||||
@unittest.SkipTest
|
@unittest.skip('')
|
||||||
def test_public_profile_download(self):
|
def test_public_profile_download(self):
|
||||||
self.L.download_profile(PUBLIC_PROFILE, profile_pic=False, fast_update=True)
|
profiles = {self.L.check_profile_id(PUBLIC_PROFILE)}
|
||||||
self.L.download_profile(PUBLIC_PROFILE, profile_pic=False, fast_update=True)
|
self.L.download_profiles(profiles, profile_pic=False, fast_update=True, raise_errors=True)
|
||||||
|
self.L.download_profiles(profiles, profile_pic=False, fast_update=True, raise_errors=True)
|
||||||
|
|
||||||
def test_public_profile_paging(self):
|
def test_public_profile_paging(self):
|
||||||
self.post_paging_test(instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).get_posts())
|
self.post_paging_test(instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).get_posts())
|
||||||
|
|
||||||
def test_profile_pic_download(self):
|
def test_profile_pic_download(self):
|
||||||
self.L.download_profile(PUBLIC_PROFILE, profile_pic_only=True)
|
self.L.download_profiles({self.L.check_profile_id(PUBLIC_PROFILE)}, posts=False, raise_errors=True)
|
||||||
|
|
||||||
def test_hashtag_download(self):
|
def test_hashtag_download(self):
|
||||||
self.L.download_hashtag(HASHTAG, NORMAL_MAX_COUNT)
|
self.L.download_hashtag(HASHTAG, NORMAL_MAX_COUNT)
|
||||||
@@ -116,13 +120,13 @@ class TestInstaloaderLoggedIn(TestInstaloaderAnonymously):
|
|||||||
super().setUp()
|
super().setUp()
|
||||||
self.L.load_session_from_file(OWN_USERNAME)
|
self.L.load_session_from_file(OWN_USERNAME)
|
||||||
|
|
||||||
@unittest.SkipTest
|
@unittest.skip('')
|
||||||
def test_stories_download(self):
|
def test_stories_download(self):
|
||||||
self.L.download_stories()
|
self.L.download_stories()
|
||||||
|
|
||||||
@unittest.SkipTest
|
@unittest.skip('')
|
||||||
def test_private_profile_download(self):
|
def test_private_profile_download(self):
|
||||||
self.L.download_profile(PRIVATE_PROFILE, download_stories=True)
|
self.L.download_profiles({self.L.check_profile_id(PRIVATE_PROFILE)}, stories=True, raise_errors=True)
|
||||||
|
|
||||||
def test_stories_paging(self):
|
def test_stories_paging(self):
|
||||||
for user_story in self.L.get_stories():
|
for user_story in self.L.get_stories():
|
||||||
@@ -141,9 +145,6 @@ class TestInstaloaderLoggedIn(TestInstaloaderAnonymously):
|
|||||||
def test_private_profile_paging(self):
|
def test_private_profile_paging(self):
|
||||||
self.post_paging_test(instaloader.Profile.from_username(self.L.context, PRIVATE_PROFILE).get_posts())
|
self.post_paging_test(instaloader.Profile.from_username(self.L.context, PRIVATE_PROFILE).get_posts())
|
||||||
|
|
||||||
def test_profile_pic_download(self):
|
|
||||||
self.L.download_profile(PUBLIC_PROFILE, profile_pic_only=True)
|
|
||||||
|
|
||||||
def test_feed_download(self):
|
def test_feed_download(self):
|
||||||
self.L.download_feed_posts(NORMAL_MAX_COUNT)
|
self.L.download_feed_posts(NORMAL_MAX_COUNT)
|
||||||
|
|
||||||
@@ -167,8 +168,8 @@ class TestInstaloaderLoggedIn(TestInstaloaderAnonymously):
|
|||||||
|
|
||||||
def test_followees_and_stories(self):
|
def test_followees_and_stories(self):
|
||||||
profile = instaloader.Profile.from_username(self.L.context, OWN_USERNAME)
|
profile = instaloader.Profile.from_username(self.L.context, OWN_USERNAME)
|
||||||
for f in islice(profile.get_followees(), PAGING_MAX_COUNT):
|
followees = set(islice(profile.get_followees(), PAGING_MAX_COUNT))
|
||||||
self.L.download_profile(f.username, profile_pic=False, download_stories_only=True)
|
self.L.download_profiles(followees, profile_pic=False, stories=True, posts=False, raise_errors=True)
|
||||||
|
|
||||||
def test_get_followees(self):
|
def test_get_followees(self):
|
||||||
profile = instaloader.Profile.from_username(self.L.context, OWN_USERNAME)
|
profile = instaloader.Profile.from_username(self.L.context, OWN_USERNAME)
|
||||||
|
|||||||
Reference in New Issue
Block a user