mirror of
https://github.com/instaloader/instaloader.git
synced 2026-10-16 21:52:17 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d938f3e2f5 | ||
|
|
374d9d0d81 | ||
|
|
3cbee1b318 | ||
|
|
dbc91cbeb1 | ||
|
|
5a42a7537f | ||
|
|
d8d268f861 | ||
|
|
463ada519e | ||
|
|
604b107586 |
@@ -0,0 +1,31 @@
|
||||
name: "Documentation"
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
name: "Documentation"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout Instaloader Repository"
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0 # needed for building docs
|
||||
- name: "Setup Python"
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: "Install Dependencies"
|
||||
run: |
|
||||
python -m pip install pipenv==2022.1.8
|
||||
pipenv sync --dev
|
||||
- name: "Build Documentation"
|
||||
run: pipenv run make -C docs html SPHINXOPTS="-W -n"
|
||||
- name: "Deploy Documentation"
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
|
||||
uses: JamesIves/github-pages-deploy-action@v4.2.5
|
||||
with:
|
||||
branch: master
|
||||
repository-name: instaloader/instaloader.github.io
|
||||
folder: docs/_build/html
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -8,24 +8,19 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"]
|
||||
steps:
|
||||
- name: Checkout Instaloader Repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0 # needed for building docs
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install pipenv==2021.5.29
|
||||
python -m pip install pipenv==2022.1.8
|
||||
pipenv sync --dev
|
||||
- name: PyLint
|
||||
run: pipenv run pylint instaloader
|
||||
- name: MyPy
|
||||
run: pipenv run mypy -m instaloader
|
||||
- name: Build Documentation
|
||||
if: matrix.python-version == '3.8'
|
||||
run: pipenv run make -C docs html SPHINXOPTS="-W -n"
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
name: "Publish Packages"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: "Publish Package"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v2
|
||||
- name: "Setup Python"
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: "Get the tagged version"
|
||||
id: get_version
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/}
|
||||
shell: bash
|
||||
- name: "Install Dependencies"
|
||||
run: |
|
||||
python -m pip install pipenv==2022.1.8
|
||||
pipenv sync --dev
|
||||
- name: "Generate Python Package"
|
||||
run: python setup.py sdist
|
||||
- name: "Publish Distribution to PyPI"
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
with:
|
||||
password: ${{ secrets.PYPI_PASSWORD }}
|
||||
- name: "Publish Distribution to AUR"
|
||||
run: deploy/arch/deploy.sh $VERSION_TAG
|
||||
env:
|
||||
VERSION_TAG: v${{ steps.get_version.outputs.VERSION }}
|
||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||
AUR_IV: ${{ secrets.AUR_IV }}
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
dist: xenial
|
||||
language: python
|
||||
python:
|
||||
- "3.6"
|
||||
- "3.7"
|
||||
install:
|
||||
- pipenv sync --dev
|
||||
script:
|
||||
- pylint instaloader
|
||||
- mypy -m instaloader
|
||||
- make -C docs html SPHINXOPTS="-W -n"
|
||||
addons:
|
||||
ssh_known_hosts: aur.archlinux.org
|
||||
deploy:
|
||||
- provider: pypi
|
||||
user: aandergr
|
||||
on:
|
||||
tags: true
|
||||
python: 3.6
|
||||
password:
|
||||
secure: KBsATtXNnEzqr0X2ALdyx2zaYj2EUfK8YjP4jEOilBELpJ4evHYXVgRkn9CKXGsD7LKbWmR3gz2rr8NZ6U42pHw0fpQHizz91MpJSKbNY7RZZh4UVWMDh81izeglchWAF+m3Wdpxpo/GBDbrZIbRQFhPgB+kjSzJo3uiCS+yVXtGG+6csJHzd0WkJjsVHz+XSdxyOiLy/qFF3nmY6ZrVrgw6WbX2YLbx9je6DxK3YRrnBqgMgH7Ui2xWWbgJCk3KKpQTMiMKnTa9mdeMJAd26YMCz1gG/X7ck6VNly/6bs5+Zq7/bU0dSolhPBhNthyccYdS0IeGPdPF3YqatblJ4fLGTfHn8oJs3/T11yIW9pvOT3UftEynPOUX+en2Qnb/3WfgwUfVts8i8DAYgEFLGEzbF5XMGy4gwAD/bZ0JW0pmgAG2PZsdOokAt+ydlNj/uxq1GFW2doZbul6k4t03p1mik6itYscvuhKoEm8Yw6wr1P9aDvMR6FFpSu4gDt2XrKmRmWkSXxpNeCfZujTjyAnISxLVQGSY7BG5K0Z2HbDtqqNz9nj4WgNGzZmngiRofCevWR+vPIDUDW7NCX03csfcFS9RRllBO/BrL7NVUV+Qp9dfTQYp+8SqYLWgHtDoXdgyMREjJaAmbx2u/TYTY8bADZ5F3vGUpv2j5hP3NTU=
|
||||
- provider: pages
|
||||
skip-cleanup: true
|
||||
github-token: $GITHUB_TOKEN
|
||||
local-dir: docs/_build/html
|
||||
repo: instaloader/instaloader.github.io
|
||||
target-branch: master
|
||||
on:
|
||||
branch: master
|
||||
python: 3.6
|
||||
- provider: script
|
||||
script: deploy/arch/deploy.sh $TRAVIS_TAG
|
||||
on:
|
||||
tags: true
|
||||
python: 3.6
|
||||
@@ -3,15 +3,16 @@
|
||||
VERSION=${1:1}
|
||||
|
||||
# do not deploy pre-releases
|
||||
echo $VERSION | grep -qe "[abc]" && exit 0
|
||||
#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
|
||||
openssl aes-256-cbc -K $AUR_KEY -iv $AUR_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
|
||||
ssh-keyscan -t rsa aur.archlinux.org >> ~/.ssh/known_hosts
|
||||
|
||||
# clone and modify AUR repo
|
||||
git clone --depth 1 ssh://aur@aur.archlinux.org/instaloader.git
|
||||
|
||||
Binary file not shown.
@@ -44,7 +44,7 @@ with open('__main__.py', 'w+') as f:
|
||||
f.writelines(lines)
|
||||
|
||||
# install dependencies and invoke PyInstaller
|
||||
commands = ["pip install pipenv==2021.5.29",
|
||||
commands = ["pip install pipenv==2022.1.8",
|
||||
f"pipenv --python {sys.version_info.major}.{sys.version_info.minor} sync --dev",
|
||||
"pipenv run pyinstaller --log-level=DEBUG instaloader.spec"]
|
||||
|
||||
|
||||
@@ -239,6 +239,13 @@ How to Download
|
||||
|
||||
.. versionadded:: 4.8
|
||||
|
||||
.. option:: --sanitize-paths
|
||||
|
||||
Force sanitization of paths so that the resulting file and directory names
|
||||
are valid on both Windows and Unix.
|
||||
|
||||
.. versionadded:: 4.9
|
||||
|
||||
.. option:: --resume-prefix prefix
|
||||
|
||||
For many targets, Instaloader is capable of resuming a previously-aborted
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Download pictures (or videos) along with their captions and other metadata from Instagram."""
|
||||
|
||||
|
||||
__version__ = '4.9b2'
|
||||
__version__ = '4.9b3'
|
||||
|
||||
|
||||
try:
|
||||
|
||||
@@ -383,6 +383,9 @@ def main():
|
||||
g_how.add_argument('--resume-prefix', metavar='PREFIX',
|
||||
help='Prefix for filenames that are used to save the information to resume an interrupted '
|
||||
'download.')
|
||||
g_how.add_argument('--sanitize-paths', action='store_true',
|
||||
help='Sanitize paths so that the resulting file and directory names are valid on both '
|
||||
'Windows and Unix.')
|
||||
g_how.add_argument('--no-resume', action='store_true',
|
||||
help='Do not resume a previously-aborted download iteration, and do not save such information '
|
||||
'when interrupted.')
|
||||
@@ -463,7 +466,8 @@ def main():
|
||||
slide=args.slide,
|
||||
fatal_status_codes=args.abort_on,
|
||||
iphone_support=not args.no_iphone,
|
||||
title_pattern=args.title_pattern)
|
||||
title_pattern=args.title_pattern,
|
||||
sanitize_paths=args.sanitize_paths)
|
||||
_main(loader,
|
||||
args.profile,
|
||||
username=args.login.lower() if args.login is not None else None,
|
||||
|
||||
+51
-21
@@ -137,24 +137,38 @@ class _ArbitraryItemFormatter(string.Formatter):
|
||||
|
||||
|
||||
class _PostPathFormatter(_ArbitraryItemFormatter):
|
||||
RESERVED: set = {'CON', 'PRN', 'AUX', 'NUL',
|
||||
'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
|
||||
'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9'}
|
||||
|
||||
def __init__(self, item: Any, force_windows_path: bool = False):
|
||||
super().__init__(item)
|
||||
self.force_windows_path = force_windows_path
|
||||
|
||||
def get_value(self, key, args, kwargs):
|
||||
ret = super().get_value(key, args, kwargs)
|
||||
if not isinstance(ret, str):
|
||||
return ret
|
||||
return self.sanitize_path(ret)
|
||||
return self.sanitize_path(ret, self.force_windows_path)
|
||||
|
||||
@staticmethod
|
||||
def sanitize_path(ret: str) -> str:
|
||||
def sanitize_path(ret: str, force_windows_path: bool = False) -> str:
|
||||
"""Replaces '/' with similar looking Division Slash and some other illegal filename characters on Windows."""
|
||||
ret = ret.replace('/', '\u2215')
|
||||
|
||||
if ret.startswith('.'):
|
||||
ret = ret.replace('.', '\u2024', 1)
|
||||
|
||||
if platform.system() == 'Windows':
|
||||
if force_windows_path or platform.system() == 'Windows':
|
||||
ret = ret.replace(':', '\uff1a').replace('<', '\ufe64').replace('>', '\ufe65').replace('\"', '\uff02')
|
||||
ret = ret.replace('\\', '\ufe68').replace('|', '\uff5c').replace('?', '\ufe16').replace('*', '\uff0a')
|
||||
ret = ret.replace('\n', ' ').replace('\r', ' ')
|
||||
root, ext = os.path.splitext(ret)
|
||||
if root.upper() in _PostPathFormatter.RESERVED:
|
||||
root += '_'
|
||||
if ext == '.':
|
||||
ext = '\u2024'
|
||||
ret = root + ext
|
||||
return ret
|
||||
|
||||
|
||||
@@ -187,6 +201,7 @@ class Instaloader:
|
||||
:param slide: :option:`--slide`
|
||||
:param fatal_status_codes: :option:`--abort-on`
|
||||
:param iphone_support: not :option:`--no-iphone`
|
||||
:param sanitize_paths: :option:`--sanitize-paths`
|
||||
|
||||
.. attribute:: context
|
||||
|
||||
@@ -216,7 +231,8 @@ class Instaloader:
|
||||
slide: Optional[str] = None,
|
||||
fatal_status_codes: Optional[List[int]] = None,
|
||||
iphone_support: bool = True,
|
||||
title_pattern: Optional[str] = None):
|
||||
title_pattern: Optional[str] = None,
|
||||
sanitize_paths: bool = False):
|
||||
|
||||
self.context = InstaloaderContext(sleep, quiet, user_agent, max_connection_attempts,
|
||||
request_timeout, rate_controller, fatal_status_codes,
|
||||
@@ -233,6 +249,7 @@ class Instaloader:
|
||||
self.title_pattern = '{date_utc}_UTC_{typename}'
|
||||
else:
|
||||
self.title_pattern = '{target}_{date_utc}_UTC_{typename}'
|
||||
self.sanitize_paths = sanitize_paths
|
||||
self.download_pictures = download_pictures
|
||||
self.download_videos = download_videos
|
||||
self.download_video_thumbnails = download_video_thumbnails
|
||||
@@ -296,7 +313,8 @@ class Instaloader:
|
||||
check_resume_bbd=self.check_resume_bbd,
|
||||
slide=self.slide,
|
||||
fatal_status_codes=self.context.fatal_status_codes,
|
||||
iphone_support=self.context.iphone_support)
|
||||
iphone_support=self.context.iphone_support,
|
||||
sanitize_paths=self.sanitize_paths)
|
||||
yield new_loader
|
||||
self.context.error_log.extend(new_loader.context.error_log)
|
||||
new_loader.context.error_log = [] # avoid double-printing of errors
|
||||
@@ -317,15 +335,25 @@ class Instaloader:
|
||||
filename_suffix: Optional[str] = None, _attempt: int = 1) -> bool:
|
||||
"""Downloads and saves picture with given url under given directory with given timestamp.
|
||||
Returns true, if file was actually downloaded, i.e. updated."""
|
||||
urlmatch = re.search('\\.[a-z0-9]*\\?', url)
|
||||
file_extension = url[-3:] if urlmatch is None else urlmatch.group(0)[1:-1]
|
||||
if filename_suffix is not None:
|
||||
filename += '_' + filename_suffix
|
||||
filename += '.' + file_extension
|
||||
if os.path.isfile(filename):
|
||||
urlmatch = re.search('\\.[a-z0-9]*\\?', url)
|
||||
file_extension = url[-3:] if urlmatch is None else urlmatch.group(0)[1:-1]
|
||||
nominal_filename = filename + '.' + file_extension
|
||||
if os.path.isfile(nominal_filename):
|
||||
self.context.log(nominal_filename + ' exists', end=' ', flush=True)
|
||||
return False
|
||||
resp = self.context.get_raw(url)
|
||||
if 'Content-Type' in resp.headers and resp.headers['Content-Type']:
|
||||
header_extension = '.' + resp.headers['Content-Type'].split(';')[0].split('/')[-1]
|
||||
header_extension = header_extension.lower().replace('jpeg', 'jpg')
|
||||
filename += header_extension
|
||||
else:
|
||||
filename = nominal_filename
|
||||
if filename != nominal_filename and os.path.isfile(filename):
|
||||
self.context.log(filename + ' exists', end=' ', flush=True)
|
||||
return False
|
||||
self.context.get_and_write_raw(url, filename)
|
||||
self.context.write_raw(resp, filename)
|
||||
os.utime(filename, (datetime.now().timestamp(), mtime.timestamp()))
|
||||
return True
|
||||
|
||||
@@ -506,9 +534,10 @@ class Instaloader:
|
||||
pic_bytes = http_response.content
|
||||
ig_filename = url.split('/')[-1].split('?')[0]
|
||||
pic_data = TitlePic(owner_profile, target, name_suffix, ig_filename, date_object)
|
||||
dirname = _PostPathFormatter(pic_data).format(self.dirname_pattern, target=target)
|
||||
filename_template = os.path.join(dirname,
|
||||
_PostPathFormatter(pic_data).format(self.title_pattern, target=target))
|
||||
dirname = _PostPathFormatter(pic_data, self.sanitize_paths).format(self.dirname_pattern, target=target)
|
||||
filename_template = os.path.join(
|
||||
dirname,
|
||||
_PostPathFormatter(pic_data, self.sanitize_paths).format(self.title_pattern, target=target))
|
||||
filename = self.__prepare_filename(filename_template, lambda: url) + ".jpg"
|
||||
content_length = http_response.headers.get('Content-Length', None)
|
||||
if os.path.isfile(filename) and (not self.context.is_logged_in or
|
||||
@@ -633,7 +662,7 @@ class Instaloader:
|
||||
"""Format filename of a :class:`Post` or :class:`StoryItem` according to ``filename-pattern`` parameter.
|
||||
|
||||
.. versionadded:: 4.1"""
|
||||
return _PostPathFormatter(item).format(self.filename_pattern, target=target)
|
||||
return _PostPathFormatter(item, self.sanitize_paths).format(self.filename_pattern, target=target)
|
||||
|
||||
def download_post(self, post: Post, target: Union[str, Path]) -> bool:
|
||||
"""
|
||||
@@ -665,7 +694,7 @@ class Instaloader:
|
||||
return False
|
||||
return True
|
||||
|
||||
dirname = _PostPathFormatter(post).format(self.dirname_pattern, target=target)
|
||||
dirname = _PostPathFormatter(post, self.sanitize_paths).format(self.dirname_pattern, target=target)
|
||||
filename_template = os.path.join(dirname, self.format_filename(post, target=target))
|
||||
filename = self.__prepare_filename(filename_template, lambda: post.url)
|
||||
|
||||
@@ -846,7 +875,7 @@ class Instaloader:
|
||||
return True
|
||||
|
||||
date_local = item.date_local
|
||||
dirname = _PostPathFormatter(item).format(self.dirname_pattern, target=target)
|
||||
dirname = _PostPathFormatter(item, self.sanitize_paths).format(self.dirname_pattern, target=target)
|
||||
filename_template = os.path.join(dirname, self.format_filename(item, target=target))
|
||||
filename = self.__prepare_filename(filename_template, lambda: item.url)
|
||||
downloaded = False
|
||||
@@ -914,8 +943,9 @@ class Instaloader:
|
||||
name = user_highlight.owner_username
|
||||
highlight_target = (filename_target
|
||||
if filename_target
|
||||
else (Path(_PostPathFormatter.sanitize_path(name)) /
|
||||
_PostPathFormatter.sanitize_path(user_highlight.title))) # type: Union[str, Path]
|
||||
else (Path(_PostPathFormatter.sanitize_path(name, self.sanitize_paths)) /
|
||||
_PostPathFormatter.sanitize_path(user_highlight.title,
|
||||
self.sanitize_paths))) # type: Union[str, Path]
|
||||
self.context.log("Retrieving highlights \"{}\" from profile {}".format(user_highlight.title, name))
|
||||
self.download_highlight_cover(user_highlight, highlight_target)
|
||||
totalcount = user_highlight.itemcount
|
||||
@@ -965,7 +995,7 @@ class Instaloader:
|
||||
else total_count)
|
||||
sanitized_target = target
|
||||
if isinstance(target, str):
|
||||
sanitized_target = _PostPathFormatter.sanitize_path(target)
|
||||
sanitized_target = _PostPathFormatter.sanitize_path(target, self.sanitize_paths)
|
||||
if takewhile is None:
|
||||
takewhile = lambda _: True
|
||||
with resumable_iteration(
|
||||
@@ -1209,8 +1239,8 @@ class Instaloader:
|
||||
tagged_posts = profile.get_tagged_posts()
|
||||
self.posts_download_loop(tagged_posts,
|
||||
target if target
|
||||
else (Path(_PostPathFormatter.sanitize_path(profile.username)) /
|
||||
_PostPathFormatter.sanitize_path(':tagged')),
|
||||
else (Path(_PostPathFormatter.sanitize_path(profile.username, self.sanitize_paths)) /
|
||||
_PostPathFormatter.sanitize_path(':tagged', self.sanitize_paths)),
|
||||
fast_update, post_filter, takewhile=posts_takewhile)
|
||||
if latest_stamps is not None and tagged_posts.first_item is not None:
|
||||
latest_stamps.set_last_tagged_timestamp(profile.username, tagged_posts.first_item.date_local)
|
||||
|
||||
@@ -8,6 +8,7 @@ from datetime import datetime
|
||||
from itertools import islice
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union
|
||||
from unicodedata import normalize
|
||||
|
||||
from . import __version__
|
||||
from .exceptions import *
|
||||
@@ -365,9 +366,9 @@ class Post:
|
||||
def caption(self) -> Optional[str]:
|
||||
"""Caption."""
|
||||
if "edge_media_to_caption" in self._node and self._node["edge_media_to_caption"]["edges"]:
|
||||
return self._node["edge_media_to_caption"]["edges"][0]["node"]["text"]
|
||||
return normalize("NFC", self._node["edge_media_to_caption"]["edges"][0]["node"]["text"])
|
||||
elif "caption" in self._node:
|
||||
return self._node["caption"]
|
||||
return normalize("NFC", self._node["caption"])
|
||||
return None
|
||||
|
||||
@property
|
||||
@@ -389,7 +390,7 @@ class Post:
|
||||
# support Unicode and a word/beginning of string delimiter at the beginning to ensure
|
||||
# that no email addresses join the list of mentions.
|
||||
# http://blog.jstassen.com/2016/03/code-regex-for-instagram-username-and-hashtags/
|
||||
mention_regex = re.compile(r"(?:^|\W|_)(?:@)(\w(?:(?:\w|(?:\.(?!\.))){0,28}(?:\w))?)")
|
||||
mention_regex = re.compile(r"(?:^|\W|_)(?:@)(\w(?:(?:\w|(?:\.(?!\.))){0,28}(?:\w))?)", re.ASCII)
|
||||
return re.findall(mention_regex, self.caption.lower())
|
||||
|
||||
@property
|
||||
|
||||
Reference in New Issue
Block a user