mirror of
https://github.com/instaloader/instaloader.git
synced 2026-10-19 23:22:16 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a125b367e3 | ||
|
|
07b30d6ebd | ||
|
|
3640e9c056 | ||
|
|
90a1b73d21 | ||
|
|
a7f89e4327 | ||
|
|
af1ec22d41 | ||
|
|
f55334cd5b | ||
|
|
b2f31e7782 | ||
|
|
68d8f65cb9 | ||
|
|
e97e4ecdc8 | ||
|
|
e77c998a94 | ||
|
|
861d7b92f1 |
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2016 Alexander Graf and André Koch-Kramer.
|
Copyright (c) 2016-2018 Alexander Graf and André Koch-Kramer.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
+1
-2
@@ -1,5 +1,4 @@
|
|||||||
Instaloader
|
.. image:: https://raw.githubusercontent.com/instaloader/instaloader/master/docs/logo_heading.png
|
||||||
===========
|
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
|
|||||||
+7
-1
@@ -50,7 +50,7 @@ clean:
|
|||||||
|
|
||||||
.PHONY: html
|
.PHONY: html
|
||||||
html:
|
html:
|
||||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
LC_ALL=C $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||||
|
|
||||||
@@ -223,3 +223,9 @@ dummy:
|
|||||||
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
|
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
|
||||||
@echo
|
@echo
|
||||||
@echo "Build finished. Dummy builder generates no files."
|
@echo "Build finished. Dummy builder generates no files."
|
||||||
|
|
||||||
|
favicon.ico: logo.svg
|
||||||
|
convert -density 384 -background transparent -define icon:auto-resize -colors 256 logo.svg favicon.ico
|
||||||
|
|
||||||
|
logo.png: logo.svg
|
||||||
|
inkscape -z -e logo.png -w 100 -h 100 logo.svg
|
||||||
|
|||||||
+2
-2
@@ -159,13 +159,13 @@ html_theme_options = {
|
|||||||
# The name of an image file (relative to this directory) to place at the top
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
# of the sidebar.
|
# of the sidebar.
|
||||||
#
|
#
|
||||||
# html_logo = None
|
html_logo = "logo.png"
|
||||||
|
|
||||||
# The name of an image file (relative to this directory) to use as a favicon of
|
# The name of an image file (relative to this directory) to use as a favicon of
|
||||||
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
# pixels large.
|
# pixels large.
|
||||||
#
|
#
|
||||||
# html_favicon = None
|
html_favicon = "favicon.ico"
|
||||||
|
|
||||||
# Add any paths that contain custom static files (such as style sheets) here,
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
# relative to this directory. They are copied after the builtin static files,
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 330 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.0 KiB |
+117
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.3 KiB |
+49
-31
@@ -6,6 +6,7 @@ import getpass
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
|
import platform
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
@@ -29,8 +30,14 @@ import requests.utils
|
|||||||
import urllib3
|
import urllib3
|
||||||
|
|
||||||
|
|
||||||
__version__ = '3.3.1'
|
__version__ = '3.3.3'
|
||||||
|
|
||||||
|
# NOTE: duplicated in README.rst and docs/index.rst
|
||||||
|
USAGE_STRING = """
|
||||||
|
{0} [--comments] [--geotags] [--stories]
|
||||||
|
[--login YOUR-USERNAME] [--fast-update]
|
||||||
|
profile | "#hashtag" | :stories | :feed | :saved
|
||||||
|
{0} --help""".format(sys.argv[0])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# pylint:disable=wrong-import-position
|
# pylint:disable=wrong-import-position
|
||||||
@@ -127,10 +134,10 @@ def mediaid_to_shortcode(mediaid: int) -> str:
|
|||||||
return b64encode(mediaid.to_bytes(9, 'big'), b'-_').decode().replace('A', ' ').lstrip().replace(' ','A')
|
return b64encode(mediaid.to_bytes(9, 'big'), b'-_').decode().replace('A', ' ').lstrip().replace(' ','A')
|
||||||
|
|
||||||
|
|
||||||
def format_string_contains_key(format_string: str, key: str) -> bool:
|
def format_string_contains_key(format_string: '_PathPattern', key: str) -> bool:
|
||||||
# pylint:disable=unused-variable
|
# pylint:disable=unused-variable
|
||||||
for literal_text, field_name, format_spec, conversion in string.Formatter().parse(format_string):
|
for literal_text, field_name, format_spec, conversion in string.Formatter().parse(format_string):
|
||||||
if field_name == key or field_name.startswith(key + '.'):
|
if field_name and (field_name == key or field_name.startswith(key + '.')):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -440,6 +447,14 @@ class Tristate(Enum):
|
|||||||
always = 2
|
always = 2
|
||||||
|
|
||||||
|
|
||||||
|
class _PathPattern(str):
|
||||||
|
"""Class overriding :meth:`str.format` for character substitution in paths for Windows, see issue #84."""
|
||||||
|
|
||||||
|
def format(self, *args: Any, **kwargs: Any) -> str:
|
||||||
|
ret = super().format(*args, **kwargs)
|
||||||
|
return ret.replace(':', '\ua789') if platform.system() == 'Windows' else ret
|
||||||
|
|
||||||
|
|
||||||
class Instaloader:
|
class Instaloader:
|
||||||
GRAPHQL_PAGE_LENGTH = 200
|
GRAPHQL_PAGE_LENGTH = 200
|
||||||
|
|
||||||
@@ -462,18 +477,19 @@ class Instaloader:
|
|||||||
self.username = None
|
self.username = None
|
||||||
self.sleep = sleep
|
self.sleep = sleep
|
||||||
self.quiet = quiet
|
self.quiet = quiet
|
||||||
self.dirname_pattern = dirname_pattern if dirname_pattern is not None else '{target}'
|
self.dirname_pattern = _PathPattern(dirname_pattern if dirname_pattern is not None else '{target}')
|
||||||
if filename_pattern is not None:
|
if filename_pattern is not None:
|
||||||
filename_pattern = re.sub(r"({(?:post\.)?date)([:}])", r"\1_utc\2", filename_pattern)
|
filename_pattern = re.sub(r"({(?:post\.)?date)([:}])", r"\1_utc\2", filename_pattern)
|
||||||
self.filename_pattern_old = filename_pattern.replace('{date_utc}', '{date_utc:%Y-%m-%d_%H-%M-%S}')
|
self.filename_pattern_old = filename_pattern.replace('{date_utc}', '{date_utc:%Y-%m-%d_%H-%M-%S}')
|
||||||
self.filename_pattern_old = re.sub(r"(?i)({(?:post\.)?date_utc:[^}]*?)_UTC",
|
self.filename_pattern_old = _PathPattern(re.sub(r"(?i)({(?:post\.)?date_utc:[^}]*?)_UTC",
|
||||||
r"\1", self.filename_pattern_old)
|
r"\1", self.filename_pattern_old))
|
||||||
filename_pattern = re.sub(r"(?i)({(date_utc|post\.date_utc):(?![^}]*UTC[^}]*).*?)}",
|
filename_pattern = re.sub(r"(?i)({(date_utc|post\.date_utc):(?![^}]*UTC[^}]*).*?)}",
|
||||||
r"\1_UTC}", filename_pattern)
|
r"\1_UTC}", filename_pattern)
|
||||||
self.filename_pattern = filename_pattern.replace('{date_utc}', '{date_utc:%Y-%m-%d_%H-%M-%S_UTC}')
|
self.filename_pattern = _PathPattern(filename_pattern.replace('{date_utc}',
|
||||||
|
'{date_utc:%Y-%m-%d_%H-%M-%S_UTC}'))
|
||||||
else:
|
else:
|
||||||
self.filename_pattern = '{date_utc:%Y-%m-%d_%H-%M-%S_UTC}'
|
self.filename_pattern = _PathPattern('{date_utc:%Y-%m-%d_%H-%M-%S_UTC}')
|
||||||
self.filename_pattern_old = '{date_utc:%Y-%m-%d_%H-%M-%S}'
|
self.filename_pattern_old = _PathPattern('{date_utc:%Y-%m-%d_%H-%M-%S}')
|
||||||
self.download_videos = download_videos
|
self.download_videos = download_videos
|
||||||
self.download_video_thumbnails = download_video_thumbnails
|
self.download_video_thumbnails = download_video_thumbnails
|
||||||
self.download_geotags = download_geotags
|
self.download_geotags = download_geotags
|
||||||
@@ -947,7 +963,7 @@ class Instaloader:
|
|||||||
def test_login(self) -> Optional[str]:
|
def test_login(self) -> Optional[str]:
|
||||||
"""Returns the Instagram username to which given :class:`requests.Session` object belongs, or None."""
|
"""Returns the Instagram username to which given :class:`requests.Session` object belongs, or None."""
|
||||||
data = self.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {})
|
data = self.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {})
|
||||||
return data["data"]["user"]["username"] if "username" in data["data"]["user"] else None
|
return data["data"]["user"]["username"] if data["data"]["user"] is not None else None
|
||||||
|
|
||||||
def login(self, user: str, passwd: str) -> None:
|
def login(self, user: str, passwd: str) -> None:
|
||||||
"""Log in to instagram with given username and password and internally store session object"""
|
"""Log in to instagram with given username and password and internally store session object"""
|
||||||
@@ -1185,30 +1201,22 @@ class Instaloader:
|
|||||||
def get_feed_posts(self) -> Iterator[Post]:
|
def get_feed_posts(self) -> Iterator[Post]:
|
||||||
"""Get Posts of the user's feed."""
|
"""Get Posts of the user's feed."""
|
||||||
|
|
||||||
data = self.get_json('', params={'__a': 1})
|
if not self.is_logged_in:
|
||||||
|
return
|
||||||
|
data = self.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {})["data"]
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
if "graphql" in data:
|
feed = data["user"]["edge_web_feed_timeline"]
|
||||||
is_edge = True
|
yield from (Post(self, edge["node"]) for edge in feed["edges"]
|
||||||
feed = data["graphql"]["user"]["edge_web_feed_timeline"]
|
if not edge["node"]["__typename"] == "GraphSuggestedUserFeedUnit")
|
||||||
elif "data" in data:
|
|
||||||
is_edge = True
|
|
||||||
feed = data["data"]["user"]["edge_web_feed_timeline"]
|
|
||||||
else:
|
|
||||||
is_edge = False
|
|
||||||
feed = data["feed"]["media"]
|
|
||||||
|
|
||||||
if is_edge:
|
|
||||||
yield from (Post(self, edge["node"]) for edge in feed["edges"])
|
|
||||||
else:
|
|
||||||
yield from (Post(self, node) for node in feed["nodes"])
|
|
||||||
|
|
||||||
if not feed["page_info"]["has_next_page"]:
|
if not feed["page_info"]["has_next_page"]:
|
||||||
break
|
break
|
||||||
data = self.graphql_query(17863003771166879, {'fetch_media_item_count': 12,
|
data = self.graphql_query("d6f4427fbe92d846298cf93df0b937d3",
|
||||||
'fetch_media_item_cursor': feed["page_info"]["end_cursor"],
|
{'fetch_media_item_count': 12,
|
||||||
'fetch_comment_count': 4,
|
'fetch_media_item_cursor': feed["page_info"]["end_cursor"],
|
||||||
'fetch_like': 10})
|
'fetch_comment_count': 4,
|
||||||
|
'fetch_like': 10,
|
||||||
|
'has_stories': False})["data"]
|
||||||
|
|
||||||
def download_feed_posts(self, max_count: int = None, fast_update: bool = False,
|
def download_feed_posts(self, max_count: int = None, fast_update: bool = False,
|
||||||
filter_func: Optional[Callable[[Post], bool]] = None) -> None:
|
filter_func: Optional[Callable[[Post], bool]] = None) -> None:
|
||||||
@@ -1244,6 +1252,8 @@ class Instaloader:
|
|||||||
def get_saved_posts(self) -> Iterator[Post]:
|
def get_saved_posts(self) -> Iterator[Post]:
|
||||||
"""Get Posts that are marked as saved by the user."""
|
"""Get Posts that are marked as saved by the user."""
|
||||||
|
|
||||||
|
if not self.is_logged_in:
|
||||||
|
return
|
||||||
data = self.get_profile_metadata(self.username)
|
data = self.get_profile_metadata(self.username)
|
||||||
user_id = data["user"]["id"]
|
user_id = data["user"]["id"]
|
||||||
|
|
||||||
@@ -1587,6 +1597,14 @@ class Instaloader:
|
|||||||
# Save session if it is useful
|
# Save session if it is useful
|
||||||
if username is not None:
|
if username is not None:
|
||||||
self.save_session_to_file(sessionfile)
|
self.save_session_to_file(sessionfile)
|
||||||
|
# User might be confused if Instaloader does nothing
|
||||||
|
if not profilelist:
|
||||||
|
if self.is_logged_in:
|
||||||
|
# Instaloader did at least save a session file
|
||||||
|
self._log("No targets were specified, thus nothing has been downloaded.")
|
||||||
|
else:
|
||||||
|
# Instloader did not do anything
|
||||||
|
self._log("usage:"+USAGE_STRING)
|
||||||
if self.error_log:
|
if self.error_log:
|
||||||
print("\nErrors occured:", file=sys.stderr)
|
print("\nErrors occured:", file=sys.stderr)
|
||||||
for err in self.error_log:
|
for err in self.error_log:
|
||||||
@@ -1594,7 +1612,7 @@ class Instaloader:
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = ArgumentParser(description=__doc__, add_help=False,
|
parser = ArgumentParser(description=__doc__, add_help=False, usage=USAGE_STRING,
|
||||||
epilog="Report issues at https://github.com/instaloader/instaloader/issues. "
|
epilog="Report issues at https://github.com/instaloader/instaloader/issues. "
|
||||||
"The complete documentation can be found at "
|
"The complete documentation can be found at "
|
||||||
"https://instaloader.github.io/.")
|
"https://instaloader.github.io/.")
|
||||||
|
|||||||
Reference in New Issue
Block a user