mirror of
https://github.com/instaloader/instaloader.git
synced 2026-10-17 06:02:18 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bc9ab55dd | ||
|
|
e64394fd25 | ||
|
|
2e482b063a | ||
|
|
0bc91b722e | ||
|
|
4264d40f19 | ||
|
|
b15a595083 | ||
|
|
71ae32c85e | ||
|
|
f5134ff95e | ||
|
|
347f01b7fc | ||
|
|
981eec9182 | ||
|
|
c54b61346e | ||
|
|
fe9c8e1604 | ||
|
|
524fb21b80 | ||
|
|
ed4add951f | ||
|
|
8e6b2d83d3 | ||
|
|
ba8faec6fe | ||
|
|
9a74d9e109 | ||
|
|
08f805482c | ||
|
|
308244b809 | ||
|
|
b0c42a2662 | ||
|
|
c5dacb5f6d | ||
|
|
3a314de191 | ||
|
|
3a95250265 | ||
|
|
2a3e5980ff | ||
|
|
8159229203 | ||
|
|
ae9a5efa85 | ||
|
|
e159065799 | ||
|
|
ed21752365 | ||
|
|
b0157d9bc1 | ||
|
|
754c3845d7 | ||
|
|
e3b8c38f8c | ||
|
|
7dac8ad1e5 | ||
|
|
b4cb9ea83a | ||
|
|
eb2accc088 | ||
|
|
66c257362d | ||
|
|
a0169b7094 | ||
|
|
0a9fccaf26 | ||
|
|
36d9f66e8e | ||
|
|
5e04063002 | ||
|
|
76e8154b58 | ||
|
|
4a969882b5 | ||
|
|
dc1eb2b193 | ||
|
|
2a95589a1d | ||
|
|
0c7398882b | ||
|
|
3b705fe5d1 | ||
|
|
3ac790d30f | ||
|
|
fbfcd5df18 | ||
|
|
b44a006344 | ||
|
|
253144203b | ||
|
|
7d0e46798e | ||
|
|
fd8245a216 | ||
|
|
3e335984f2 | ||
|
|
98791fb0aa | ||
|
|
65b12d650c | ||
|
|
1fb98b066a |
@@ -8,22 +8,23 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout Instaloader Repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # needed for building docs
|
||||
- name: "Setup Python"
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
cache: 'pipenv'
|
||||
- name: "Install Dependencies"
|
||||
run: |
|
||||
python -m pip install pipenv==2023.12.1
|
||||
python -m pip install pipenv==2024.2.0
|
||||
pipenv --python `python --version | grep -Eo '3\.[0-9]+'` sync --dev
|
||||
- name: "Build Documentation"
|
||||
run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` 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
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
branch: master
|
||||
repository-name: instaloader/instaloader.github.io
|
||||
|
||||
@@ -9,17 +9,18 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
|
||||
steps:
|
||||
- name: Checkout Instaloader Repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pipenv'
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install pipenv==2023.12.1
|
||||
python -m pip install pipenv==2024.2.0
|
||||
pipenv --python `python --version | grep -Eo '3\.[0-9]+'` sync --dev
|
||||
- name: PyLint
|
||||
run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run pylint instaloader
|
||||
|
||||
@@ -11,23 +11,23 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: "Setup Python"
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
- name: "Get the tagged version"
|
||||
id: get_version
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/}
|
||||
run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
- name: "Install Dependencies"
|
||||
run: python -m pip install setuptools
|
||||
run: python -m pip install -U setuptools wheel build
|
||||
- name: "Generate Python Package"
|
||||
run: python setup.py sdist
|
||||
run: python -m build
|
||||
- name: "Publish Distribution to PyPI"
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_PASSWORD }}
|
||||
- name: "Publish Distribution to AUR"
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Mark stale issues and pull requests
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "10 14 * * *"
|
||||
- cron: "55 7 * * *"
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
only-labels: 'question'
|
||||
@@ -21,14 +21,14 @@ jobs:
|
||||
days-before-stale: 21
|
||||
days-before-close: -1
|
||||
remove-stale-when-updated: false
|
||||
- uses: actions/stale@v1
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
stale-issue-message: 'There has been no activity on this issue for an extended period of time. This issue will be closed after further 14 days of inactivity.'
|
||||
stale-pr-message: 'There has been no activity on this Pull Request for an extended period of time. This Pull Request will be closed after further 14 days of inactivity.'
|
||||
stale-issue-label: 'stale'
|
||||
stale-pr-label: 'stale'
|
||||
exempt-issue-label: 'leave open'
|
||||
exempt-pr-label: 'leave open'
|
||||
exempt-issue-labels: 'leave open'
|
||||
exempt-pr-labels: 'leave open'
|
||||
days-before-stale: 189
|
||||
days-before-close: 14
|
||||
|
||||
@@ -8,41 +8,32 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout Instaloader repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: 'pipenv'
|
||||
architecture: x64
|
||||
- name: Get the tagged version
|
||||
id: get_version
|
||||
env:
|
||||
GITHUB_REF: ${{ github.ref }}
|
||||
run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\/v/}
|
||||
run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
|
||||
shell: bash
|
||||
- name: Run custom python script for EXE creation
|
||||
env:
|
||||
VERSION_TAG: v${{ steps.get_version.outputs.VERSION }}
|
||||
run: python deploy/windows/create_exe.py
|
||||
- name: Create draft release
|
||||
- name: Create draft release and upload EXE as release asset
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: v${{ steps.get_version.outputs.VERSION }}
|
||||
release_name: Version ${{ steps.get_version.outputs.VERSION }}
|
||||
name: Version ${{ steps.get_version.outputs.VERSION }}
|
||||
draft: true
|
||||
prerelease: contains(steps.get_version.outputs.VERSION, 'a') || contains(steps.get_version.outputs.VERSION, 'b') || contains(steps.get_version.outputs.VERSION, 'c')
|
||||
- name: Upload EXE as release asset
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
||||
id: upload-release-asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./instaloader-v${{ steps.get_version.outputs.VERSION }}-windows-standalone.zip
|
||||
asset_name: instaloader-v${{ steps.get_version.outputs.VERSION }}-windows-standalone.zip
|
||||
asset_content_type: application/zip
|
||||
files: ./instaloader-v${{ steps.get_version.outputs.VERSION }}-windows-standalone.zip
|
||||
|
||||
@@ -351,7 +351,7 @@ single-line-if-stmt=no
|
||||
allow-any-import-level=
|
||||
|
||||
# Allow explicit reexports by alias from a package __init__.
|
||||
allow-reexport-from-package=no
|
||||
allow-reexport-from-package=yes
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
Generated
+157
-156
@@ -24,11 +24,11 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f",
|
||||
"sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"
|
||||
"sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b",
|
||||
"sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2024.2.2"
|
||||
"version": "==2024.7.4"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
@@ -128,11 +128,11 @@
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca",
|
||||
"sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"
|
||||
"sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc",
|
||||
"sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.6"
|
||||
"version": "==3.7"
|
||||
},
|
||||
"jeepney": {
|
||||
"hashes": [
|
||||
@@ -224,30 +224,30 @@
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
|
||||
"sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
|
||||
"sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760",
|
||||
"sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.31.0"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.32.3"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d",
|
||||
"sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"
|
||||
"sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472",
|
||||
"sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.2.1"
|
||||
"version": "==2.2.2"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"alabaster": {
|
||||
"hashes": [
|
||||
"sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65",
|
||||
"sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"
|
||||
"sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e",
|
||||
"sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==0.7.16"
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"altgraph": {
|
||||
"hashes": [
|
||||
@@ -258,27 +258,27 @@
|
||||
},
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:951798f922990137ac090c53af473db7ab4e70c770e6d7fae0cec59f74411819",
|
||||
"sha256:ac248253bfa4bd924a0de213707e7ebeeb3138abeb48d798784ead1e56d419d4"
|
||||
"sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a",
|
||||
"sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25"
|
||||
],
|
||||
"markers": "python_full_version >= '3.8.0'",
|
||||
"version": "==3.1.0"
|
||||
"version": "==3.2.4"
|
||||
},
|
||||
"babel": {
|
||||
"hashes": [
|
||||
"sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363",
|
||||
"sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"
|
||||
"sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b",
|
||||
"sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.14.0"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.16.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f",
|
||||
"sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"
|
||||
"sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b",
|
||||
"sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2024.2.2"
|
||||
"version": "==2024.7.4"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
@@ -387,19 +387,19 @@
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6",
|
||||
"sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"
|
||||
"sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f",
|
||||
"sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.20.1"
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==0.21.2"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca",
|
||||
"sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"
|
||||
"sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc",
|
||||
"sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.6"
|
||||
"version": "==3.7"
|
||||
},
|
||||
"imagesize": {
|
||||
"hashes": [
|
||||
@@ -419,11 +419,11 @@
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa",
|
||||
"sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"
|
||||
"sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369",
|
||||
"sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.1.3"
|
||||
"version": "==3.1.4"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
@@ -501,37 +501,37 @@
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6",
|
||||
"sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913",
|
||||
"sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129",
|
||||
"sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc",
|
||||
"sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974",
|
||||
"sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374",
|
||||
"sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150",
|
||||
"sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03",
|
||||
"sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9",
|
||||
"sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02",
|
||||
"sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89",
|
||||
"sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2",
|
||||
"sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d",
|
||||
"sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3",
|
||||
"sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612",
|
||||
"sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e",
|
||||
"sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3",
|
||||
"sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e",
|
||||
"sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd",
|
||||
"sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04",
|
||||
"sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed",
|
||||
"sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185",
|
||||
"sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf",
|
||||
"sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b",
|
||||
"sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4",
|
||||
"sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f",
|
||||
"sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"
|
||||
"sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54",
|
||||
"sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a",
|
||||
"sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72",
|
||||
"sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69",
|
||||
"sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b",
|
||||
"sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe",
|
||||
"sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4",
|
||||
"sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd",
|
||||
"sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0",
|
||||
"sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525",
|
||||
"sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2",
|
||||
"sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c",
|
||||
"sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5",
|
||||
"sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de",
|
||||
"sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74",
|
||||
"sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c",
|
||||
"sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e",
|
||||
"sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58",
|
||||
"sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b",
|
||||
"sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417",
|
||||
"sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411",
|
||||
"sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb",
|
||||
"sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03",
|
||||
"sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca",
|
||||
"sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8",
|
||||
"sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08",
|
||||
"sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.9.0"
|
||||
"version": "==1.11.1"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
@@ -543,11 +543,11 @@
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5",
|
||||
"sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"
|
||||
"sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002",
|
||||
"sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==24.0"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==24.1"
|
||||
},
|
||||
"pefile": {
|
||||
"hashes": [
|
||||
@@ -560,78 +560,79 @@
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068",
|
||||
"sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"
|
||||
"sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee",
|
||||
"sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==4.2.0"
|
||||
"version": "==4.2.2"
|
||||
},
|
||||
"psutil": {
|
||||
"hashes": [
|
||||
"sha256:02615ed8c5ea222323408ceba16c60e99c3f91639b07da6373fb7e6539abc56d",
|
||||
"sha256:05806de88103b25903dff19bb6692bd2e714ccf9e668d050d144012055cbca73",
|
||||
"sha256:26bd09967ae00920df88e0352a91cff1a78f8d69b3ecabbfe733610c0af486c8",
|
||||
"sha256:27cc40c3493bb10de1be4b3f07cae4c010ce715290a5be22b98493509c6299e2",
|
||||
"sha256:36f435891adb138ed3c9e58c6af3e2e6ca9ac2f365efe1f9cfef2794e6c93b4e",
|
||||
"sha256:50187900d73c1381ba1454cf40308c2bf6f34268518b3f36a9b663ca87e65e36",
|
||||
"sha256:611052c4bc70432ec770d5d54f64206aa7203a101ec273a0cd82418c86503bb7",
|
||||
"sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c",
|
||||
"sha256:7d79560ad97af658a0f6adfef8b834b53f64746d45b403f225b85c5c2c140eee",
|
||||
"sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421",
|
||||
"sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf",
|
||||
"sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81",
|
||||
"sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0",
|
||||
"sha256:bd1184ceb3f87651a67b2708d4c3338e9b10c5df903f2e3776b62303b26cb631",
|
||||
"sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4",
|
||||
"sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8"
|
||||
"sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35",
|
||||
"sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0",
|
||||
"sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c",
|
||||
"sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1",
|
||||
"sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3",
|
||||
"sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c",
|
||||
"sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd",
|
||||
"sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3",
|
||||
"sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0",
|
||||
"sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2",
|
||||
"sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6",
|
||||
"sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d",
|
||||
"sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c",
|
||||
"sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0",
|
||||
"sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132",
|
||||
"sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14",
|
||||
"sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
|
||||
"version": "==5.9.8"
|
||||
"version": "==6.0.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c",
|
||||
"sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"
|
||||
"sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199",
|
||||
"sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.17.2"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.18.0"
|
||||
},
|
||||
"pyinstaller": {
|
||||
"hashes": [
|
||||
"sha256:0dae0edbe6d667b6b0ccd8c97a148f86474a82da7ce582296f9025f4c7242ec6",
|
||||
"sha256:1b3b7d6d3b18d76a833fd5a4d7f4544c5e2c2a4db4a728ea191e62f69d5cc33c",
|
||||
"sha256:5f432f3fdef053989e0a44134e483131c533dab7637e6afd80c3f7c26e6dbcc9",
|
||||
"sha256:61865eee5e0d8f8252722f6d001baec497b7cee79ebe62c33a6ba86ba0c7010d",
|
||||
"sha256:6cfee8a74ea2d3a1dc8e99e732a87b314739dc14363778143caac31f8aee9039",
|
||||
"sha256:6ffd76a0194dac4df5e66dcfccc7b597f3eaa40ef9a3f63548f260aa2c187512",
|
||||
"sha256:7c76bfcb624803c311fa8fb137e4780d0ec86d11b7d90a8f43f185e2554afdcc",
|
||||
"sha256:81ec15c0deb8c7a0f95bea85b49eecc2df1bdeaf5fe487a41d97de6b0ad29dff",
|
||||
"sha256:9d828213aea5401bb33a36ca396f8dc76a59a25bce1d76a13c9ad94ba29fbe42",
|
||||
"sha256:a54968df2228f0128607b1dced41bbff94149d459987fb5cd1a41893e9bb85df",
|
||||
"sha256:b1e55113c5a40cb7041c908a57f212f3ebd3e444dbb245ca2f91d86a76dabec5",
|
||||
"sha256:e1266498893ce1d6cc7337e8d2acbf7905a10ed2b7c8377270117d6b7b922fc4"
|
||||
"sha256:2bf4de17a1c63c0b797b38e13bfb4d03b5ee7c0a68e28b915a7eaacf6b76087f",
|
||||
"sha256:43709c70b1da8441a730327a8ed362bfcfdc3d42c1bf89f3e2b0a163cc4e7d33",
|
||||
"sha256:4e3e50743c091a06e6d01c59bdd6d03967b453ee5384a9e790759be4129db4a4",
|
||||
"sha256:5ced2e83acf222b936ea94abc5a5cc96588705654b39138af8fb321d9cf2b954",
|
||||
"sha256:7bf0c13c5a8560c89540746ae742f4f4b82290e95a6b478374d9f34959fe25d6",
|
||||
"sha256:a0f378f64ad0655d11ade9fde7877e7573fd3d5066231608ce7dfa9040faecdd",
|
||||
"sha256:b041be2fe78da47a269604d62c940d68c62f9a3913bdf64af4123f7689d47099",
|
||||
"sha256:da994aba14c5686db88796684de265a8665733b4df09b939f7ebdf097d18df72",
|
||||
"sha256:f15c1ef11ed5ceb32447dfbdab687017d6adbef7fc32aa359d584369bfe56eda",
|
||||
"sha256:f18a3d551834ef8fb7830d48d4cc1527004d0e6b51ded7181e78374ad6111846",
|
||||
"sha256:f2fc568de3d6d2a176716a3fc9f20da06d351e8bea5ddd10ecb5659fce3a05b0",
|
||||
"sha256:f4a75c552facc2e2a370f1e422b971b5e5cdb4058ff38cea0235aa21fc0b378f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version < '3.13' and python_version >= '3.8'",
|
||||
"version": "==6.5.0"
|
||||
"version": "==6.9.0"
|
||||
},
|
||||
"pyinstaller-hooks-contrib": {
|
||||
"hashes": [
|
||||
"sha256:6701752d525e1f4eda1eaec2c2affc206171e15c7a4e188a152fcf3ed3308024",
|
||||
"sha256:d18657c29267c63563a96b8fc78db6ba9ae40af6702acb2f8c871df12c75b60b"
|
||||
"sha256:0057fe9a5c398d3f580e73e58793a1d4a8315ca91c3df01efea1c14ed557825a",
|
||||
"sha256:29b68d878ab739e967055b56a93eb9b58e529d5b054fbab7a2f2bacf80cef3e2"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2024.3"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2024.8"
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:507a5b60953874766d8a366e8e8c7af63e058b26345cfcb5f91f89d987fd6b74",
|
||||
"sha256:6a69beb4a6f63debebaab0a3477ecd0f559aa726af4954fc948c51f7a2549e23"
|
||||
"sha256:03c8e3baa1d9fb995b12c1dbe00aa6c4bcef210c2a2634374aedeb22fb4a8f8f",
|
||||
"sha256:a5d01678349454806cff6d886fb072294f56a58c4761278c97fb557d708e1eb3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_full_version >= '3.8.0'",
|
||||
"version": "==3.1.0"
|
||||
"version": "==3.2.6"
|
||||
},
|
||||
"pywin32-ctypes": {
|
||||
"hashes": [
|
||||
@@ -644,20 +645,20 @@
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
|
||||
"sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
|
||||
"sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760",
|
||||
"sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.31.0"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.32.3"
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e",
|
||||
"sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"
|
||||
"sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1",
|
||||
"sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==69.2.0"
|
||||
"version": "==72.1.0"
|
||||
},
|
||||
"snowballstemmer": {
|
||||
"hashes": [
|
||||
@@ -668,45 +669,45 @@
|
||||
},
|
||||
"sphinx": {
|
||||
"hashes": [
|
||||
"sha256:1e09160a40b956dc623c910118fa636da93bd3ca0b9876a7b3df90f07d691560",
|
||||
"sha256:9a5160e1ea90688d5963ba09a2dcd8bdd526620edbb65c328728f1b2228d5ab5"
|
||||
"sha256:0cce1ddcc4fd3532cf1dd283bc7d886758362c5c1de6598696579ce96d8ffa5b",
|
||||
"sha256:56173572ae6c1b9a38911786e206a110c9749116745873feae4f9ce88e59391d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==7.2.6"
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==8.0.2"
|
||||
},
|
||||
"sphinx-autodoc-typehints": {
|
||||
"hashes": [
|
||||
"sha256:12c0e161f6fe191c2cdfd8fa3caea271f5387d9fbc67ebcd6f4f1f24ce880993",
|
||||
"sha256:7f2cdac2e70fd9787926b6e9e541cd4ded1e838d2b46fda2a1bb0a75ec5b7f3a"
|
||||
"sha256:b7058e8c5831e5598afca1a78fda0695d3291388d954464a6e480c36198680c0",
|
||||
"sha256:fde3d888949bd0a91207cf1e54afda58121dbb4bf1f183d0cc78a0826654c974"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.0.0"
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==2.2.3"
|
||||
},
|
||||
"sphinxcontrib-applehelp": {
|
||||
"hashes": [
|
||||
"sha256:c40a4f96f3776c4393d933412053962fac2b84f4c99a7982ba42e09576a70619",
|
||||
"sha256:cb61eb0ec1b61f349e5cc36b2028e9e7ca765be05e49641c97241274753067b4"
|
||||
"sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1",
|
||||
"sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==1.0.8"
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"sphinxcontrib-devhelp": {
|
||||
"hashes": [
|
||||
"sha256:6485d09629944511c893fa11355bda18b742b83a2b181f9a009f7e500595c90f",
|
||||
"sha256:9893fd3f90506bc4b97bdb977ceb8fbd823989f4316b28c3841ec128544372d3"
|
||||
"sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad",
|
||||
"sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==1.0.6"
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"sphinxcontrib-htmlhelp": {
|
||||
"hashes": [
|
||||
"sha256:0dc87637d5de53dd5eec3a6a01753b1ccf99494bd756aafecd74b4fa9e729015",
|
||||
"sha256:393f04f112b4d2f53d93448d4bce35842f62b307ccdc549ec1585e950bc35e04"
|
||||
"sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8",
|
||||
"sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==2.0.5"
|
||||
"version": "==2.1.0"
|
||||
},
|
||||
"sphinxcontrib-jquery": {
|
||||
"hashes": [
|
||||
@@ -727,19 +728,19 @@
|
||||
},
|
||||
"sphinxcontrib-qthelp": {
|
||||
"hashes": [
|
||||
"sha256:053dedc38823a80a7209a80860b16b722e9e0209e32fea98c90e4e6624588ed6",
|
||||
"sha256:e2ae3b5c492d58fcbd73281fbd27e34b8393ec34a073c792642cd8e529288182"
|
||||
"sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab",
|
||||
"sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==1.0.7"
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"sphinxcontrib-serializinghtml": {
|
||||
"hashes": [
|
||||
"sha256:326369b8df80a7d2d8d7f99aa5ac577f51ea51556ed974e7716cfd4fca3f6cb7",
|
||||
"sha256:93f3f5dc458b91b192fe10c397e324f262cf163d79f3282c158e8436a2c4511f"
|
||||
"sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331",
|
||||
"sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==1.1.10"
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"tomli": {
|
||||
"hashes": [
|
||||
@@ -752,36 +753,36 @@
|
||||
},
|
||||
"tomlkit": {
|
||||
"hashes": [
|
||||
"sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b",
|
||||
"sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"
|
||||
"sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72",
|
||||
"sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.12.4"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.13.0"
|
||||
},
|
||||
"types-requests": {
|
||||
"hashes": [
|
||||
"sha256:47872893d65a38e282ee9f277a4ee50d1b28bd592040df7d1fdaffdf3779937d",
|
||||
"sha256:b1c1b66abfb7fa79aae09097a811c4aa97130eb8831c60e47aee4ca344731ca5"
|
||||
"sha256:90c079ff05e549f6bf50e02e910210b98b8ff1ebdd18e19c873cd237737c1358",
|
||||
"sha256:f754283e152c752e46e70942fa2a146b5bc70393522257bb85bd1ef7e019dcc3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.31.0.20240311"
|
||||
"version": "==2.32.0.20240712"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475",
|
||||
"sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"
|
||||
"sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d",
|
||||
"sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==4.10.0"
|
||||
"version": "==4.12.2"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d",
|
||||
"sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"
|
||||
"sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472",
|
||||
"sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.2.1"
|
||||
"version": "==2.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -53,7 +53,7 @@
|
||||
::
|
||||
|
||||
instaloader [--comments] [--geotags]
|
||||
[--stories] [--highlights] [--tagged] [--igtv]
|
||||
[--stories] [--highlights] [--tagged] [--reels] [--igtv]
|
||||
[--login YOUR-USERNAME] [--fast-update]
|
||||
profile | "#hashtag" | :stories | :feed | :saved
|
||||
|
||||
|
||||
@@ -5,9 +5,13 @@ pkgbase = instaloader
|
||||
url = https://instaloader.github.io/
|
||||
arch = any
|
||||
license = MIT
|
||||
makedepends = python-build
|
||||
makedepends = python-installer
|
||||
makedepends = python-setuptools
|
||||
depends = python>=3.8
|
||||
depends = python-requests>=2.4
|
||||
makedepends = python-wheel
|
||||
depends = python
|
||||
depends = python-requests
|
||||
optdepends = python-browser-cookie3: Import session cookies from browser
|
||||
options = !emptydirs
|
||||
source = instaloader-{{version}}.tar.gz::https://codeload.github.com/instaloader/instaloader/tar.gz/v{{version}}
|
||||
sha512sums = {{hash}}
|
||||
|
||||
@@ -8,20 +8,30 @@ arch=('any')
|
||||
url="https://instaloader.github.io/"
|
||||
license=('MIT')
|
||||
groups=()
|
||||
depends=('python>=3.8' 'python-requests>=2.4')
|
||||
makedepends=('python-setuptools')
|
||||
depends=(
|
||||
python
|
||||
python-requests
|
||||
)
|
||||
makedepends=(
|
||||
python-build
|
||||
python-installer
|
||||
python-setuptools
|
||||
python-wheel
|
||||
)
|
||||
optdepends=(
|
||||
'python-browser-cookie3: Import session cookies from browser'
|
||||
)
|
||||
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
|
||||
python -m build --wheel --no-isolation
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$srcdir/$pkgname-$pkgver"
|
||||
install -Dm644 LICENSE "$pkgdir"/usr/share/licenses/$pkgname/LICENSE
|
||||
python setup.py install --root "$pkgdir/" --skip-build
|
||||
python -m installer --destdir="$pkgdir" dist/*.whl
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ with open('__main__.py', 'w+') as f:
|
||||
f.writelines(lines)
|
||||
|
||||
# install dependencies and invoke PyInstaller
|
||||
commands = ["pip install pipenv==2023.12.1",
|
||||
commands = ["pip install pipenv==2024.2.0",
|
||||
f"pipenv --python {sys.version_info.major}.{sys.version_info.minor} sync --dev",
|
||||
"pipenv run pyinstaller --log-level=DEBUG instaloader.spec"]
|
||||
|
||||
|
||||
+50
-12
@@ -75,7 +75,7 @@ What to Download
|
||||
Instaloader supports the following targets:
|
||||
|
||||
- ``profile``
|
||||
Public profile, or private profile with :option:`--login`.
|
||||
Public profile, or private profile with :ref:`login<login>`.
|
||||
|
||||
If an already-downloaded profile has been renamed, Instaloader automatically
|
||||
finds it by its unique ID and renames the folder accordingly.
|
||||
@@ -92,37 +92,41 @@ Instaloader supports the following targets:
|
||||
- :option:`--tagged`
|
||||
to **download posts where the user is tagged**, and
|
||||
|
||||
- :option:`--reels`
|
||||
to **download Reels videos**.
|
||||
|
||||
- :option:`--igtv`
|
||||
to **download IGTV videos**.
|
||||
|
||||
- ``"#hashtag"``
|
||||
Posts with a certain **hashtag** (the quotes are usually necessary).
|
||||
Posts with a certain **hashtag** (the quotes are usually necessary). Requires :ref:`login<login>`.
|
||||
|
||||
- ``%location id``
|
||||
Posts tagged with a given location; the location ID is the numerical ID
|
||||
Instagram labels a location with (e.g.
|
||||
\https://www.instagram.com/explore/locations/**362629379**/plymouth-naval-memorial/).
|
||||
Requires :option:`--login`.
|
||||
Requires :ref:`login<login>`.
|
||||
|
||||
.. versionadded:: 4.2
|
||||
|
||||
- ``:stories``
|
||||
The currently-visible **stories** of your followees (requires
|
||||
:option:`--login`).
|
||||
:ref:`login<login>`).
|
||||
|
||||
- ``:feed``
|
||||
Your **feed** (requires :option:`--login`).
|
||||
Your **feed** (requires :ref:`login<login>`).
|
||||
|
||||
- ``:saved``
|
||||
Posts which are marked as **saved** (requires :option:`--login`).
|
||||
Posts which are marked as **saved** (requires :ref:`login<login>`).
|
||||
|
||||
- ``@profile``
|
||||
All profiles that are followed by ``profile``, i.e. the *followees* of
|
||||
``profile`` (requires :option:`--login`).
|
||||
``profile`` (requires :ref:`login<login>`).
|
||||
|
||||
- ``-post``
|
||||
Replace **post** with the post's shortcode to download single post. Must be preceded by ``--`` in
|
||||
the argument list to not be mistaken as an option flag::
|
||||
the argument list to not be mistaken as an option flag. For example, to download the post
|
||||
https://www.instagram.com/p/**B_K4CykAOtf**, run the command::
|
||||
|
||||
instaloader -- -B_K4CykAOtf
|
||||
|
||||
@@ -140,7 +144,7 @@ downloads the pictures and videos and their captions. You can specify
|
||||
|
||||
- :option:`--geotags`
|
||||
**download geotags** of each post and save them as
|
||||
Google Maps link (requires :option:`--login`),
|
||||
Google Maps link (requires :ref:`login<login>`),
|
||||
|
||||
For a reference of all supported command line options, see
|
||||
:ref:`command-line-options`.
|
||||
@@ -231,7 +235,7 @@ The filter string must be a
|
||||
where the attributes from :class:`Post` or
|
||||
:class:`StoryItem` respectively are defined.
|
||||
|
||||
Id est, the following attributes can be used with both
|
||||
The following attributes can be used with both
|
||||
:option:`--post-filter` and :option:`--storyitem-filter`:
|
||||
|
||||
- :attr:`~Post.owner_username` (str), :attr:`~Post.owner_id` (int)
|
||||
@@ -255,7 +259,7 @@ Id est, the following attributes can be used with both
|
||||
As :option:`--post-filter`, the following attributes can be used additionally:
|
||||
|
||||
- :attr:`~Post.viewer_has_liked` (bool)
|
||||
Whether user (with :option:`--login`) has liked given post. To download the
|
||||
Whether user (with :ref:`login<login>`) has liked given post. To download the
|
||||
pictures from your feed that you have liked::
|
||||
|
||||
instaloader --login=your_username --post-filter=viewer_has_liked :feed
|
||||
@@ -307,13 +311,47 @@ the post's caption::
|
||||
|
||||
instaloader --post-metadata-txt="{likes} likes." <target>
|
||||
|
||||
Note that with this feature, it is possible to easily and fastly extract
|
||||
Note that with this feature, it is possible to easily and quickly extract
|
||||
additional metadata of already-downloaded posts, by reimporting their JSON
|
||||
files. Say, you now also want to export the number of comments the Posts had
|
||||
when they were downloaded::
|
||||
|
||||
instaloader --post-metadata-txt="{likes} likes, {comments} comments." <target>/*.json.xz
|
||||
|
||||
.. _exit_codes:
|
||||
|
||||
Exit codes
|
||||
^^^^^^^^^^
|
||||
|
||||
Different exit codes are used to indicate different kinds of error:
|
||||
|
||||
0
|
||||
No error, all downloads were successful.
|
||||
|
||||
1
|
||||
A non-fatal error happened. One or more posts, or even one or more
|
||||
profiles could not be downloaded, but execution was not stopped. The
|
||||
errors are repeated at the end of the log for easy access.
|
||||
|
||||
2
|
||||
Command-line error. An unrecognized option was passed, or an invalid
|
||||
combination of options, for example. No interaction with Instagram
|
||||
was made.
|
||||
|
||||
3
|
||||
Login error. It was not possible to login. Downloads were not
|
||||
attempted.
|
||||
|
||||
4
|
||||
Fatal download error. Downloads were interrupted and no further
|
||||
attempts were made. Happens when a response with one of the status
|
||||
codes in the :option:`--abort-on` option were passed, or when
|
||||
Instagram logs the user out during downloads.
|
||||
|
||||
5
|
||||
Interrupted by the user. Happens when the user presses Control-C or
|
||||
sends SIGINT to the process.
|
||||
|
||||
.. _instaloader-as-cronjob:
|
||||
|
||||
Instaloader as Cronjob
|
||||
|
||||
+19
-9
@@ -26,7 +26,7 @@ Targets
|
||||
^^^^^^^
|
||||
|
||||
Specify a list of targets. For each of these, Instaloader creates a folder and
|
||||
stores all posts along with the pictures's captions there.
|
||||
stores all posts along with the pictures' captions there.
|
||||
|
||||
.. include:: basic-usage.rst
|
||||
:start-after: targets-start
|
||||
@@ -61,13 +61,13 @@ What to Download of each Post
|
||||
|
||||
Download geotags when available. Geotags are stored as a text file with
|
||||
the location's name and a Google Maps link. This requires an additional
|
||||
request to the Instagram server for each picture. Requires :option:`--login`.
|
||||
request to the Instagram server for each picture. Requires :ref:`login<login>`.
|
||||
|
||||
.. option:: --comments, -C
|
||||
|
||||
Download and update comments for each post. This requires an additional
|
||||
request to the Instagram server for each post, which is why it is disabled by
|
||||
default.
|
||||
default. Requires :ref:`login<login>`.
|
||||
|
||||
.. option:: --no-captions
|
||||
|
||||
@@ -116,12 +116,12 @@ What to Download of each Profile
|
||||
.. option:: --stories, -s
|
||||
|
||||
Also download stories of each profile that is downloaded. Requires
|
||||
:option:`--login`.
|
||||
:ref:`login<login>`.
|
||||
|
||||
.. option:: --highlights
|
||||
|
||||
Also download highlights of each profile that is downloaded. Requires
|
||||
:option:`--login`.
|
||||
:ref:`login<login>`.
|
||||
|
||||
.. versionadded:: 4.1
|
||||
|
||||
@@ -131,6 +131,12 @@ What to Download of each Profile
|
||||
|
||||
.. versionadded:: 4.1
|
||||
|
||||
.. option:: --reels
|
||||
|
||||
Also download Reels videos.
|
||||
|
||||
.. versionadded:: 4.14
|
||||
|
||||
.. option:: --igtv
|
||||
|
||||
Also download IGTV videos.
|
||||
@@ -183,6 +189,7 @@ Which Posts to Download
|
||||
Do not attempt to download more than COUNT posts. Applies to
|
||||
``#hashtag``, ``%location_id``, ``:feed``, and ``:saved``.
|
||||
|
||||
.. _login:
|
||||
|
||||
Login (Download Private Profiles)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -192,6 +199,9 @@ profiles. To login, pass the :option:`--login` option. Your session cookie (not
|
||||
password!) will be saved to a local file to be reused next time you want
|
||||
Instaloader to login.
|
||||
|
||||
Instead of :option:`--login`, it is possible to use
|
||||
:option:`--load-cookies` to import a session from a browser.
|
||||
|
||||
.. option:: --login YOUR-USERNAME, -l YOUR-USERNAME
|
||||
|
||||
Login name (profile name) for your Instagram account.
|
||||
@@ -202,10 +212,10 @@ Instaloader to login.
|
||||
This feature requires the browser_cookie3 library.
|
||||
Compatible with :option:`--cookiefile` if you want to load cookies from browser profiles.
|
||||
Incompatible with :option:`--login` due to potential username mismatch between user input and browser login.
|
||||
Supported browsers: Chrome, Firefox, Edge, Opera, Safari, Brave.
|
||||
Supported browsers: Brave, Chrome, Chromium, Edge, Firefox, LibreWolf, Opera, Opera_GX, Safari and Vivaldi.
|
||||
|
||||
After loading the cookies run the :option:`--login` option as it is required to download high quality media
|
||||
and to make full use of Instaloader's features.
|
||||
In subsequent runs, you can just use :option:`--login` to reuse the
|
||||
same session, which is saved by Instaloader.
|
||||
|
||||
.. versionadded:: 4.11
|
||||
|
||||
@@ -300,7 +310,7 @@ How to Download
|
||||
.. option:: --user-agent USER_AGENT
|
||||
|
||||
User Agent to use for HTTP requests. Per default, Instaloader pretends being
|
||||
Chrome/120 on Linux.
|
||||
Chrome/127 on Linux.
|
||||
|
||||
.. option:: --max-connection-attempts N
|
||||
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ See :ref:`install` for more options on how to install Instaloader.
|
||||
::
|
||||
|
||||
instaloader [--comments] [--geotags]
|
||||
[--stories] [--highlights] [--tagged] [--igtv]
|
||||
[--stories] [--highlights] [--tagged] [--reels] [--igtv]
|
||||
[--login YOUR-USERNAME] [--fast-update]
|
||||
profile | "#hashtag" | %location_id |
|
||||
:stories | :feed | :saved
|
||||
|
||||
@@ -2,7 +2,7 @@ Exceptions
|
||||
^^^^^^^^^^
|
||||
|
||||
.. module:: instaloader
|
||||
:noindex:
|
||||
:no-index:
|
||||
|
||||
.. highlight:: python
|
||||
|
||||
@@ -27,16 +27,26 @@ Exceptions
|
||||
|
||||
.. autoexception:: LoginRequiredException
|
||||
|
||||
.. autoexception:: LoginException
|
||||
|
||||
.. versionadded:: 4.12
|
||||
|
||||
.. autoexception:: TwoFactorAuthRequiredException
|
||||
|
||||
.. versionadded:: 4.2
|
||||
|
||||
.. versionchanged:: 4.12
|
||||
Inherits LoginException
|
||||
|
||||
.. autoexception:: InvalidArgumentException
|
||||
|
||||
.. autoexception:: BadResponseException
|
||||
|
||||
.. autoexception:: BadCredentialsException
|
||||
|
||||
.. versionchanged:: 4.12
|
||||
Inherits LoginException
|
||||
|
||||
.. autoexception:: PostChangedException
|
||||
|
||||
.. autoexception:: QueryReturnedNotFoundException
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. module:: instaloader
|
||||
:noindex:
|
||||
:no-index:
|
||||
|
||||
.. highlight:: python
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. module:: instaloader
|
||||
:noindex:
|
||||
:no-index:
|
||||
|
||||
.. highlight:: python
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ Resumable Iterations
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. module:: instaloader
|
||||
:noindex:
|
||||
:no-index:
|
||||
|
||||
.. highlight:: python
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ Instagram Structures
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. module:: instaloader
|
||||
:noindex:
|
||||
:no-index:
|
||||
|
||||
.. highlight:: python
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ happen under normal conditions, consider adjusting the
|
||||
There have been observations that services, that in their nature offer
|
||||
promiscuous IP addresses, such as cloud, VPN and public proxy services, might be
|
||||
subject to significantly stricter limits for anonymous access. However,
|
||||
logged-in accesses (see :option:`--login`) do not seem to be affected.
|
||||
:ref:`logged-in accesses<login>` do not seem to be affected.
|
||||
|
||||
Instaloader allows to adjust the rate controlling behavior by overriding
|
||||
:class:`instaloader.RateController`.
|
||||
|
||||
+24
-9
@@ -1,7 +1,7 @@
|
||||
"""Download pictures (or videos) along with their captions and other metadata from Instagram."""
|
||||
|
||||
|
||||
__version__ = '4.11'
|
||||
__version__ = '4.14.2'
|
||||
|
||||
|
||||
try:
|
||||
@@ -13,11 +13,26 @@ else:
|
||||
win_unicode_console.enable()
|
||||
|
||||
from .exceptions import *
|
||||
from .instaloader import Instaloader
|
||||
from .instaloadercontext import InstaloaderContext, RateController
|
||||
from .lateststamps import LatestStamps
|
||||
from .nodeiterator import NodeIterator, FrozenNodeIterator, resumable_iteration
|
||||
from .structures import (Hashtag, Highlight, Post, PostSidecarNode, PostComment, PostCommentAnswer, PostLocation,
|
||||
Profile, Story, StoryItem, TopSearchResults, TitlePic,
|
||||
load_structure_from_file, save_structure_to_file,
|
||||
load_structure, get_json_structure)
|
||||
from .instaloader import Instaloader as Instaloader
|
||||
from .instaloadercontext import (InstaloaderContext as InstaloaderContext,
|
||||
RateController as RateController)
|
||||
from .lateststamps import LatestStamps as LatestStamps
|
||||
from .nodeiterator import (NodeIterator as NodeIterator,
|
||||
FrozenNodeIterator as FrozenNodeIterator,
|
||||
resumable_iteration as resumable_iteration)
|
||||
from .structures import (Hashtag as Hashtag,
|
||||
Highlight as Highlight,
|
||||
Post as Post,
|
||||
PostSidecarNode as PostSidecarNode,
|
||||
PostComment as PostComment,
|
||||
PostCommentAnswer as PostCommentAnswer,
|
||||
PostLocation as PostLocation,
|
||||
Profile as Profile,
|
||||
Story as Story,
|
||||
StoryItem as StoryItem,
|
||||
TopSearchResults as TopSearchResults,
|
||||
TitlePic as TitlePic,
|
||||
load_structure_from_file as load_structure_from_file,
|
||||
save_structure_to_file as save_structure_to_file,
|
||||
load_structure as load_structure,
|
||||
get_json_structure as get_json_structure)
|
||||
|
||||
+125
-71
@@ -5,12 +5,12 @@ import datetime
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import textwrap
|
||||
from argparse import ArgumentParser, ArgumentTypeError, SUPPRESS
|
||||
from enum import IntEnum
|
||||
from typing import List, Optional
|
||||
|
||||
from . import (AbortDownloadException, BadCredentialsException, Instaloader, InstaloaderException,
|
||||
InvalidArgumentException, Post, Profile, ProfileNotExistsException, StoryItem,
|
||||
InvalidArgumentException, LoginException, Post, Profile, ProfileNotExistsException, StoryItem,
|
||||
TwoFactorAuthRequiredException, __version__, load_structure_from_file)
|
||||
from .instaloader import (get_default_session_filename, get_default_stamps_filename)
|
||||
from .instaloadercontext import default_user_agent
|
||||
@@ -22,13 +22,22 @@ except ImportError:
|
||||
bc3_library = False
|
||||
|
||||
|
||||
class ExitCode(IntEnum):
|
||||
SUCCESS = 0
|
||||
NON_FATAL_ERROR = 1
|
||||
INIT_FAILURE = 2
|
||||
LOGIN_FAILURE = 3
|
||||
DOWNLOAD_ABORTED = 4
|
||||
USER_ABORTED = 5
|
||||
UNEXPECTED_ERROR = 99
|
||||
|
||||
def usage_string():
|
||||
# NOTE: duplicated in README.rst and docs/index.rst
|
||||
argv0 = os.path.basename(sys.argv[0])
|
||||
argv0 = "instaloader" if argv0 == "__main__.py" else argv0
|
||||
return """
|
||||
{0} [--comments] [--geotags]
|
||||
{2:{1}} [--stories] [--highlights] [--tagged] [--igtv]
|
||||
{2:{1}} [--stories] [--highlights] [--tagged] [--reels] [--igtv]
|
||||
{2:{1}} [--login YOUR-USERNAME] [--fast-update]
|
||||
{2:{1}} profile | "#hashtag" | %%location_id | :stories | :feed | :saved
|
||||
{0} --help""".format(argv0, len(argv0), '')
|
||||
@@ -75,18 +84,22 @@ def filterstr_to_filterfunc(filter_str: str, item_type: type):
|
||||
|
||||
def get_cookies_from_instagram(domain, browser, cookie_file='', cookie_name=''):
|
||||
supported_browsers = {
|
||||
"chrome": browser_cookie3.chrome,
|
||||
"firefox": browser_cookie3.firefox,
|
||||
"edge": browser_cookie3.edge,
|
||||
"brave": browser_cookie3.brave,
|
||||
"chrome": browser_cookie3.chrome,
|
||||
"chromium": browser_cookie3.chromium,
|
||||
"edge": browser_cookie3.edge,
|
||||
"firefox": browser_cookie3.firefox,
|
||||
"librewolf": browser_cookie3.librewolf,
|
||||
"opera": browser_cookie3.opera,
|
||||
"safari": browser_cookie3.safari
|
||||
"opera_gx": browser_cookie3.opera_gx,
|
||||
"safari": browser_cookie3.safari,
|
||||
"vivaldi": browser_cookie3.vivaldi,
|
||||
}
|
||||
|
||||
if browser not in supported_browsers:
|
||||
print("Loading cookies from the specified browser failed")
|
||||
print("Supported browsers are Chrome, Firefox, Edge, Brave, Opera and Safari")
|
||||
return {}
|
||||
raise InvalidArgumentException("Loading cookies from the specified browser failed\n"
|
||||
"Supported browsers are Brave, Chrome, Chromium, Edge, Firefox, LibreWolf, "
|
||||
"Opera, Opera_GX, Safari and Vivaldi")
|
||||
|
||||
cookies = {}
|
||||
browser_cookies = list(supported_browsers[browser](cookie_file=cookie_file))
|
||||
@@ -98,7 +111,8 @@ def get_cookies_from_instagram(domain, browser, cookie_file='', cookie_name=''):
|
||||
if cookies:
|
||||
print(f"Cookies loaded successfully from {browser}")
|
||||
else:
|
||||
print(f"No cookies found for Instagram in {browser}, Are you logged in succesfully in {browser}?")
|
||||
raise LoginException(f"No cookies found for Instagram in {browser}, "
|
||||
f"Are you logged in successfully in {browser}?")
|
||||
|
||||
if cookie_name:
|
||||
return cookies.get(cookie_name, {})
|
||||
@@ -112,12 +126,10 @@ def import_session(browser, instaloader, cookiefile):
|
||||
instaloader.context.update_cookies(cookie)
|
||||
username = instaloader.test_login()
|
||||
if not username:
|
||||
raise SystemExit(f"Not logged in. Are you logged in successfully in {browser}?")
|
||||
raise LoginException(f"Not logged in. Are you logged in successfully in {browser}?")
|
||||
instaloader.context.username = username
|
||||
print(f"{username} has been successfully logged in.")
|
||||
next_step_text = (f"Next: Run instaloader --login={username} as it is required to download high quality media "
|
||||
"and to make full use of instaloader's features.")
|
||||
print(textwrap.fill(next_step_text))
|
||||
print(f"Next time use --login={username} to reuse the same session.")
|
||||
|
||||
|
||||
def _main(instaloader: Instaloader, targetlist: List[str],
|
||||
@@ -127,13 +139,14 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
||||
download_stories: bool = False,
|
||||
download_highlights: bool = False,
|
||||
download_tagged: bool = False,
|
||||
download_reels: bool = False,
|
||||
download_igtv: bool = False,
|
||||
fast_update: bool = False,
|
||||
latest_stamps_file: Optional[str] = None,
|
||||
max_count: Optional[int] = None, post_filter_str: Optional[str] = None,
|
||||
storyitem_filter_str: Optional[str] = None,
|
||||
browser: Optional[str] = None,
|
||||
cookiefile: Optional[str] = None) -> None:
|
||||
cookiefile: Optional[str] = None) -> ExitCode:
|
||||
"""Download set of profiles, hashtags etc. and handle logging in and session files if desired."""
|
||||
# Parse and generate filter function
|
||||
post_filter = None
|
||||
@@ -152,7 +165,7 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
||||
if browser and bc3_library:
|
||||
import_session(browser.lower(), instaloader, cookiefile)
|
||||
elif browser and not bc3_library:
|
||||
raise SystemExit("browser_cookie3 library is needed to load cookies from browsers")
|
||||
raise InvalidArgumentException("browser_cookie3 library is needed to load cookies from browsers")
|
||||
# Login, if desired
|
||||
if username is not None:
|
||||
if not re.match(r"^[A-Za-z0-9._]+$", username):
|
||||
@@ -181,14 +194,19 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
||||
print(err, file=sys.stderr)
|
||||
pass
|
||||
else:
|
||||
instaloader.interactive_login(username)
|
||||
try:
|
||||
instaloader.interactive_login(username)
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrupted by user.", file=sys.stderr)
|
||||
return ExitCode.USER_ABORTED
|
||||
instaloader.context.log("Logged in as %s." % username)
|
||||
# since 4.2.9 login is required for geotags
|
||||
if instaloader.download_geotags and not instaloader.context.is_logged_in:
|
||||
instaloader.context.error("Warning: Use --login to download geotags of posts.")
|
||||
instaloader.context.error("Warning: Login is required to download geotags of posts.")
|
||||
# Try block for KeyboardInterrupt (save session on ^C)
|
||||
profiles = set()
|
||||
anonymous_retry_profiles = set()
|
||||
exit_code = ExitCode.SUCCESS
|
||||
try:
|
||||
# Generate set of profiles, already downloading non-profile targets
|
||||
for target in targetlist:
|
||||
@@ -241,11 +259,13 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
||||
instaloader.download_saved_posts(fast_update=fast_update, max_count=max_count,
|
||||
post_filter=post_filter)
|
||||
elif re.match(r"^[A-Za-z0-9._]+$", target):
|
||||
download_profile_content = download_posts or download_tagged or download_reels or download_igtv
|
||||
try:
|
||||
profile = instaloader.check_profile_id(target, latest_stamps)
|
||||
if instaloader.context.is_logged_in and profile.has_blocked_viewer:
|
||||
if download_profile_pic or ((download_posts or download_tagged or download_igtv)
|
||||
and not profile.is_private):
|
||||
if download_profile_pic or (
|
||||
download_profile_content and not profile.is_private
|
||||
):
|
||||
raise ProfileNotExistsException("{} blocked you; But we download her anonymously."
|
||||
.format(target))
|
||||
else:
|
||||
@@ -255,8 +275,7 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
||||
except ProfileNotExistsException as err:
|
||||
# Not only our profile.has_blocked_viewer condition raises ProfileNotExistsException,
|
||||
# check_profile_id() also does, since access to blocked profile may be responded with 404.
|
||||
if instaloader.context.is_logged_in and (download_profile_pic or download_posts or
|
||||
download_tagged or download_igtv):
|
||||
if instaloader.context.is_logged_in and (download_profile_pic or download_profile_content):
|
||||
instaloader.context.log(err)
|
||||
instaloader.context.log("Trying again anonymously, helps in case you are just blocked.")
|
||||
with instaloader.anonymous_copy() as anonymous_loader:
|
||||
@@ -279,23 +298,42 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
||||
' '.join([p.username for p in profiles])))
|
||||
if instaloader.context.iphone_support and profiles and (download_profile_pic or download_posts) and \
|
||||
not instaloader.context.is_logged_in:
|
||||
instaloader.context.log("Hint: Use --login to download higher-quality versions of pictures.")
|
||||
instaloader.download_profiles(profiles,
|
||||
download_profile_pic, download_posts, download_tagged, download_igtv,
|
||||
download_highlights, download_stories,
|
||||
fast_update, post_filter, storyitem_filter, latest_stamps=latest_stamps)
|
||||
instaloader.context.log("Hint: Login to download higher-quality versions of pictures.")
|
||||
instaloader.download_profiles(
|
||||
profiles,
|
||||
download_profile_pic,
|
||||
download_posts,
|
||||
download_tagged,
|
||||
download_igtv,
|
||||
download_highlights,
|
||||
download_stories,
|
||||
fast_update,
|
||||
post_filter,
|
||||
storyitem_filter,
|
||||
latest_stamps=latest_stamps,
|
||||
reels=download_reels,
|
||||
)
|
||||
if anonymous_retry_profiles:
|
||||
instaloader.context.log("Downloading anonymously: {}"
|
||||
.format(' '.join([p.username for p in anonymous_retry_profiles])))
|
||||
with instaloader.anonymous_copy() as anonymous_loader:
|
||||
anonymous_loader.download_profiles(anonymous_retry_profiles,
|
||||
download_profile_pic, download_posts, download_tagged, download_igtv,
|
||||
fast_update=fast_update, post_filter=post_filter,
|
||||
latest_stamps=latest_stamps)
|
||||
anonymous_loader.download_profiles(
|
||||
anonymous_retry_profiles,
|
||||
download_profile_pic,
|
||||
download_posts,
|
||||
download_tagged,
|
||||
download_igtv,
|
||||
fast_update=fast_update,
|
||||
post_filter=post_filter,
|
||||
latest_stamps=latest_stamps,
|
||||
reels=download_reels
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\nInterrupted by user.", file=sys.stderr)
|
||||
exit_code = ExitCode.USER_ABORTED
|
||||
except AbortDownloadException as exc:
|
||||
print("\nDownload aborted: {}.".format(exc), file=sys.stderr)
|
||||
exit_code = ExitCode.DOWNLOAD_ABORTED
|
||||
# Save session if it is useful
|
||||
if instaloader.context.is_logged_in:
|
||||
instaloader.save_session_to_file(sessionfile)
|
||||
@@ -307,6 +345,8 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
||||
else:
|
||||
# Instaloader did not do anything
|
||||
instaloader.context.log("usage:" + usage_string())
|
||||
exit_code = ExitCode.INIT_FAILURE
|
||||
return exit_code
|
||||
|
||||
|
||||
def main():
|
||||
@@ -322,17 +362,17 @@ def main():
|
||||
help="Download profile. If an already-downloaded profile has been renamed, Instaloader "
|
||||
"automatically finds it by its unique ID and renames the folder likewise.")
|
||||
g_targets.add_argument('_at_profile', nargs='*', metavar="@profile",
|
||||
help="Download all followees of profile. Requires --login. "
|
||||
help="Download all followees of profile. Requires login. "
|
||||
"Consider using :feed rather than @yourself.")
|
||||
g_targets.add_argument('_hashtag', nargs='*', metavar='"#hashtag"', help="Download #hashtag.")
|
||||
g_targets.add_argument('_location', nargs='*', metavar='%location_id',
|
||||
help="Download %%location_id. Requires --login.")
|
||||
help="Download %%location_id. Requires login.")
|
||||
g_targets.add_argument('_feed', nargs='*', metavar=":feed",
|
||||
help="Download pictures from your feed. Requires --login.")
|
||||
help="Download pictures from your feed. Requires login.")
|
||||
g_targets.add_argument('_stories', nargs='*', metavar=":stories",
|
||||
help="Download the stories of your followees. Requires --login.")
|
||||
help="Download the stories of your followees. Requires login.")
|
||||
g_targets.add_argument('_saved', nargs='*', metavar=":saved",
|
||||
help="Download the posts that you marked as saved. Requires --login.")
|
||||
help="Download the posts that you marked as saved. Requires login.")
|
||||
g_targets.add_argument('_singlepost', nargs='*', metavar="-- -shortcode",
|
||||
help="Download the post with the given shortcode")
|
||||
g_targets.add_argument('_json', nargs='*', metavar="filename.json[.xz]",
|
||||
@@ -363,11 +403,11 @@ def main():
|
||||
help='Download geotags when available. Geotags are stored as a '
|
||||
'text file with the location\'s name and a Google Maps link. '
|
||||
'This requires an additional request to the Instagram '
|
||||
'server for each picture. Requires --login.')
|
||||
'server for each picture. Requires login.')
|
||||
g_post.add_argument('-C', '--comments', action='store_true',
|
||||
help='Download and update comments for each post. '
|
||||
'This requires an additional request to the Instagram '
|
||||
'server for each post, which is why it is disabled by default.')
|
||||
'server for each post, which is why it is disabled by default. Requires login.')
|
||||
g_post.add_argument('--no-captions', action='store_true',
|
||||
help='Do not create txt files.')
|
||||
g_post.add_argument('--post-metadata-txt', action='append',
|
||||
@@ -381,13 +421,15 @@ def main():
|
||||
g_post.add_argument('--no-compress-json', action='store_true',
|
||||
help='Do not xz compress JSON files, rather create pretty formatted JSONs.')
|
||||
g_prof.add_argument('-s', '--stories', action='store_true',
|
||||
help='Also download stories of each profile that is downloaded. Requires --login.')
|
||||
help='Also download stories of each profile that is downloaded. Requires login.')
|
||||
g_prof.add_argument('--stories-only', action='store_true',
|
||||
help=SUPPRESS)
|
||||
g_prof.add_argument('--highlights', action='store_true',
|
||||
help='Also download highlights of each profile that is downloaded. Requires --login.')
|
||||
help='Also download highlights of each profile that is downloaded. Requires login.')
|
||||
g_prof.add_argument('--tagged', action='store_true',
|
||||
help='Also download posts where each profile is tagged.')
|
||||
g_prof.add_argument('--reels', action='store_true',
|
||||
help='Also download Reels videos.')
|
||||
g_prof.add_argument('--igtv', action='store_true',
|
||||
help='Also download IGTV videos.')
|
||||
|
||||
@@ -417,7 +459,8 @@ def main():
|
||||
'Instaloader can login to Instagram. This allows downloading private profiles. '
|
||||
'To login, pass the --login option. Your session cookie (not your password!) '
|
||||
'will be saved to a local file to be reused next time you want Instaloader '
|
||||
'to login.')
|
||||
'to login. Instead of --login, the --load-cookies option can be used to '
|
||||
'import a session from a browser.')
|
||||
g_login.add_argument('-l', '--login', metavar='YOUR-USERNAME',
|
||||
help='Login name (profile name) for your Instagram account.')
|
||||
g_login.add_argument('-b', '--load-cookies', metavar='BROWSER-NAME',
|
||||
@@ -484,15 +527,15 @@ def main():
|
||||
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
if args.login is None and (args.stories or args.stories_only):
|
||||
print("--login=USERNAME required to download stories.", file=sys.stderr)
|
||||
if (args.login is None and args.load_cookies is None) and (args.stories or args.stories_only):
|
||||
print("Login is required to download stories.", file=sys.stderr)
|
||||
args.stories = False
|
||||
if args.stories_only:
|
||||
raise SystemExit(1)
|
||||
raise InvalidArgumentException()
|
||||
|
||||
if ':feed-all' in args.profile or ':feed-liked' in args.profile:
|
||||
raise SystemExit(":feed-all and :feed-liked were removed. Use :feed as target and "
|
||||
"eventually --post-filter=viewer_has_liked.")
|
||||
raise InvalidArgumentException(":feed-all and :feed-liked were removed. Use :feed as target and "
|
||||
"eventually --post-filter=viewer_has_liked.")
|
||||
|
||||
post_metadata_txt_pattern = '\n'.join(args.post_metadata_txt) if args.post_metadata_txt else None
|
||||
storyitem_metadata_txt_pattern = '\n'.join(args.storyitem_metadata_txt) if args.storyitem_metadata_txt else None
|
||||
@@ -502,18 +545,18 @@ def main():
|
||||
post_metadata_txt_pattern = ''
|
||||
storyitem_metadata_txt_pattern = ''
|
||||
else:
|
||||
raise SystemExit("--no-captions and --post-metadata-txt or --storyitem-metadata-txt given; "
|
||||
"That contradicts.")
|
||||
raise InvalidArgumentException("--no-captions and --post-metadata-txt or --storyitem-metadata-txt "
|
||||
"given; That contradicts.")
|
||||
|
||||
if args.no_resume and args.resume_prefix:
|
||||
raise SystemExit("--no-resume and --resume-prefix given; That contradicts.")
|
||||
raise InvalidArgumentException("--no-resume and --resume-prefix given; That contradicts.")
|
||||
resume_prefix = (args.resume_prefix if args.resume_prefix else 'iterator') if not args.no_resume else None
|
||||
|
||||
if args.no_pictures and args.fast_update:
|
||||
raise SystemExit('--no-pictures and --fast-update cannot be used together.')
|
||||
raise InvalidArgumentException('--no-pictures and --fast-update cannot be used together.')
|
||||
|
||||
if args.login and args.load_cookies:
|
||||
raise SystemExit('--load-cookies and --login cannot be used together.')
|
||||
raise InvalidArgumentException('--load-cookies and --login cannot be used together.')
|
||||
|
||||
# Determine what to download
|
||||
download_profile_pic = not args.no_profile_pic or args.profile_pic_only
|
||||
@@ -538,27 +581,38 @@ def main():
|
||||
iphone_support=not args.no_iphone,
|
||||
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,
|
||||
password=args.password,
|
||||
sessionfile=args.sessionfile,
|
||||
download_profile_pic=download_profile_pic,
|
||||
download_posts=download_posts,
|
||||
download_stories=download_stories,
|
||||
download_highlights=args.highlights,
|
||||
download_tagged=args.tagged,
|
||||
download_igtv=args.igtv,
|
||||
fast_update=args.fast_update,
|
||||
latest_stamps_file=args.latest_stamps,
|
||||
max_count=int(args.count) if args.count is not None else None,
|
||||
post_filter_str=args.post_filter,
|
||||
storyitem_filter_str=args.storyitem_filter,
|
||||
browser=args.load_cookies,
|
||||
cookiefile=args.cookiefile)
|
||||
exit_code = _main(loader,
|
||||
args.profile,
|
||||
username=args.login.lower() if args.login is not None else None,
|
||||
password=args.password,
|
||||
sessionfile=args.sessionfile,
|
||||
download_profile_pic=download_profile_pic,
|
||||
download_posts=download_posts,
|
||||
download_stories=download_stories,
|
||||
download_highlights=args.highlights,
|
||||
download_tagged=args.tagged,
|
||||
download_reels=args.reels,
|
||||
download_igtv=args.igtv,
|
||||
fast_update=args.fast_update,
|
||||
latest_stamps_file=args.latest_stamps,
|
||||
max_count=int(args.count) if args.count is not None else None,
|
||||
post_filter_str=args.post_filter,
|
||||
storyitem_filter_str=args.storyitem_filter,
|
||||
browser=args.load_cookies,
|
||||
cookiefile=args.cookiefile)
|
||||
loader.close()
|
||||
if loader.has_stored_errors:
|
||||
exit_code = ExitCode.NON_FATAL_ERROR
|
||||
except InvalidArgumentException as err:
|
||||
print(err, file=sys.stderr)
|
||||
exit_code = ExitCode.INIT_FAILURE
|
||||
except LoginException as err:
|
||||
print(err, file=sys.stderr)
|
||||
exit_code = ExitCode.LOGIN_FAILURE
|
||||
except InstaloaderException as err:
|
||||
raise SystemExit("Fatal error: %s" % err) from err
|
||||
print("Fatal error: %s" % err)
|
||||
exit_code = ExitCode.UNEXPECTED_ERROR
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -33,7 +33,11 @@ class LoginRequiredException(InstaloaderException):
|
||||
pass
|
||||
|
||||
|
||||
class TwoFactorAuthRequiredException(InstaloaderException):
|
||||
class LoginException(InstaloaderException):
|
||||
pass
|
||||
|
||||
|
||||
class TwoFactorAuthRequiredException(LoginException):
|
||||
pass
|
||||
|
||||
|
||||
@@ -45,7 +49,7 @@ class BadResponseException(InstaloaderException):
|
||||
pass
|
||||
|
||||
|
||||
class BadCredentialsException(InstaloaderException):
|
||||
class BadCredentialsException(LoginException):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
+76
-15
@@ -77,13 +77,13 @@ def _requires_login(func: Callable) -> Callable:
|
||||
@wraps(func)
|
||||
def call(instaloader, *args, **kwargs):
|
||||
if not instaloader.context.is_logged_in:
|
||||
raise LoginRequiredException("--login=USERNAME required.")
|
||||
raise LoginRequiredException("Login required.")
|
||||
return func(instaloader, *args, **kwargs)
|
||||
return call
|
||||
|
||||
|
||||
def _retry_on_connection_error(func: Callable) -> Callable:
|
||||
"""Decorator to retry the function max_connection_attemps number of times.
|
||||
"""Decorator to retry the function max_connection_attempts number of times.
|
||||
|
||||
Herewith-decorated functions need an ``_attempt`` keyword argument.
|
||||
|
||||
@@ -643,11 +643,16 @@ class Instaloader:
|
||||
def login(self, user: str, passwd: str) -> None:
|
||||
"""Log in to instagram with given username and password and internally store session object.
|
||||
|
||||
:raises InvalidArgumentException: If the provided username does not exist.
|
||||
:raises BadCredentialsException: If the provided password is wrong.
|
||||
:raises ConnectionException: If connection to Instagram failed.
|
||||
:raises TwoFactorAuthRequiredException: First step of 2FA login done, now call
|
||||
:meth:`Instaloader.two_factor_login`."""
|
||||
:meth:`Instaloader.two_factor_login`.
|
||||
:raises LoginException: An error happened during login (for example, an invalid response was received).
|
||||
Or if the provided username does not exist.
|
||||
|
||||
.. versionchanged:: 4.12
|
||||
Raises LoginException instead of ConnectionException when an error happens.
|
||||
Raises LoginException instead of InvalidArgumentException when the username does not exist.
|
||||
"""
|
||||
self.context.login(user, passwd)
|
||||
|
||||
def two_factor_login(self, two_factor_code) -> None:
|
||||
@@ -717,7 +722,7 @@ class Instaloader:
|
||||
# Download the image(s) / video thumbnail and videos within sidecars if desired
|
||||
downloaded = True
|
||||
if post.typename == 'GraphSidecar':
|
||||
if self.download_pictures or self.download_videos:
|
||||
if (self.download_pictures or self.download_videos) and post.mediacount > 0:
|
||||
if not _all_already_downloaded(
|
||||
filename_template, enumerate(
|
||||
(post.get_is_videos()[i]
|
||||
@@ -861,8 +866,9 @@ class Instaloader:
|
||||
last_scraped = latest_stamps.get_last_story_timestamp(name)
|
||||
scraped_timestamp = datetime.now().astimezone()
|
||||
for item in user_story.get_items():
|
||||
if latest_stamps is not None and item.date_local <= last_scraped:
|
||||
break
|
||||
if latest_stamps is not None:
|
||||
if item.date_local <= last_scraped:
|
||||
break
|
||||
if storyitem_filter is not None and not storyitem_filter(item):
|
||||
self.context.log("<{} skipped>".format(item), flush=True)
|
||||
continue
|
||||
@@ -1209,6 +1215,7 @@ class Instaloader:
|
||||
Use :meth:`Hashtag.get_posts_resumable`."""
|
||||
return Hashtag.from_name(self.context, hashtag).get_posts_resumable()
|
||||
|
||||
@_requires_login
|
||||
def download_hashtag(self, hashtag: Union[Hashtag, str],
|
||||
max_count: Optional[int] = None,
|
||||
post_filter: Optional[Callable[[Post], bool]] = None,
|
||||
@@ -1275,6 +1282,32 @@ class Instaloader:
|
||||
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)
|
||||
|
||||
def download_reels(self, profile: Profile, fast_update: bool = False,
|
||||
post_filter: Optional[Callable[[Post], bool]] = None,
|
||||
latest_stamps: Optional[LatestStamps] = None) -> None:
|
||||
"""Download reels videos of a profile.
|
||||
|
||||
.. versionadded:: 4.14.0
|
||||
|
||||
"""
|
||||
self.context.log("Retrieving reels videos for profile {}.".format(profile.username))
|
||||
posts_takewhile: Optional[Callable[[Post], bool]] = None
|
||||
if latest_stamps is not None:
|
||||
last_scraped = latest_stamps.get_last_reels_timestamp(profile.username)
|
||||
posts_takewhile = lambda p: p.date_local > last_scraped
|
||||
reels = profile.get_reels()
|
||||
self.posts_download_loop(
|
||||
reels,
|
||||
profile.username,
|
||||
fast_update,
|
||||
post_filter,
|
||||
owner_profile=profile,
|
||||
takewhile=posts_takewhile,
|
||||
possibly_pinned=3,
|
||||
)
|
||||
if latest_stamps is not None and reels.first_item is not None:
|
||||
latest_stamps.set_last_reels_timestamp(profile.username, reels.first_item.date_local)
|
||||
|
||||
def download_igtv(self, profile: Profile, fast_update: bool = False,
|
||||
post_filter: Optional[Callable[[Post], bool]] = None,
|
||||
latest_stamps: Optional[LatestStamps] = None) -> None:
|
||||
@@ -1404,7 +1437,9 @@ class Instaloader:
|
||||
post_filter: Optional[Callable[[Post], bool]] = None,
|
||||
storyitem_filter: Optional[Callable[[Post], bool]] = None,
|
||||
raise_errors: bool = False,
|
||||
latest_stamps: Optional[LatestStamps] = None):
|
||||
latest_stamps: Optional[LatestStamps] = None,
|
||||
max_count: Optional[int] = None,
|
||||
reels: bool = False):
|
||||
"""High-level method to download set of profiles.
|
||||
|
||||
:param profiles: Set of profiles to download.
|
||||
@@ -1421,6 +1456,8 @@ class Instaloader:
|
||||
Whether :exc:`LoginRequiredException` and :exc:`PrivateProfileNotFollowedException` should be raised or
|
||||
catched and printed with :meth:`InstaloaderContext.error_catcher`.
|
||||
:param latest_stamps: :option:`--latest-stamps`.
|
||||
:param max_count: Maximum count of posts to download.
|
||||
:param reels: :option:`--reels`.
|
||||
|
||||
.. versionadded:: 4.1
|
||||
|
||||
@@ -1429,6 +1466,12 @@ class Instaloader:
|
||||
|
||||
.. versionchanged:: 4.8
|
||||
Add `latest_stamps` parameter.
|
||||
|
||||
.. versionchanged:: 4.13
|
||||
Add `max_count` parameter.
|
||||
|
||||
.. versionchanged:: 4.14
|
||||
Add `reels` parameter.
|
||||
"""
|
||||
|
||||
@contextmanager
|
||||
@@ -1460,7 +1503,7 @@ class Instaloader:
|
||||
if tagged or igtv or highlights or posts:
|
||||
if (not self.context.is_logged_in and
|
||||
profile.is_private):
|
||||
raise LoginRequiredException("--login=USERNAME required.")
|
||||
raise LoginRequiredException("Login required.")
|
||||
if (self.context.username != profile.username and
|
||||
profile.is_private and
|
||||
not profile.followed_by_viewer):
|
||||
@@ -1472,6 +1515,12 @@ class Instaloader:
|
||||
self.download_tagged(profile, fast_update=fast_update, post_filter=post_filter,
|
||||
latest_stamps=latest_stamps)
|
||||
|
||||
# Download reels, if requested
|
||||
if reels:
|
||||
with self.context.error_catcher('Download reels of {}'.format(profile_name)):
|
||||
self.download_reels(profile, fast_update=fast_update, post_filter=post_filter,
|
||||
latest_stamps=latest_stamps)
|
||||
|
||||
# Download IGTV, if requested
|
||||
if igtv:
|
||||
with self.context.error_catcher('Download IGTV of {}'.format(profile_name)):
|
||||
@@ -1494,7 +1543,7 @@ class Instaloader:
|
||||
posts_to_download = profile.get_posts()
|
||||
self.posts_download_loop(posts_to_download, profile_name, fast_update, post_filter,
|
||||
total_count=profile.mediacount, owner_profile=profile,
|
||||
takewhile=posts_takewhile, possibly_pinned=3)
|
||||
takewhile=posts_takewhile, possibly_pinned=3, max_count=max_count)
|
||||
if latest_stamps is not None and posts_to_download.first_item is not None:
|
||||
latest_stamps.set_last_post_timestamp(profile_name,
|
||||
posts_to_download.first_item.date_local)
|
||||
@@ -1582,11 +1631,16 @@ class Instaloader:
|
||||
def interactive_login(self, username: str) -> None:
|
||||
"""Logs in and internally stores session, asking user for password interactively.
|
||||
|
||||
:raises LoginRequiredException: when in quiet mode.
|
||||
:raises InvalidArgumentException: If the provided username does not exist.
|
||||
:raises ConnectionException: If connection to Instagram failed."""
|
||||
:raises InvalidArgumentException: when in quiet mode.
|
||||
:raises LoginException: If the provided username does not exist.
|
||||
:raises ConnectionException: If connection to Instagram failed.
|
||||
|
||||
.. versionchanged:: 4.12
|
||||
Raises InvalidArgumentException instead of LoginRequiredException when in quiet mode.
|
||||
Raises LoginException instead of InvalidArgumentException when the username does not exist.
|
||||
"""
|
||||
if self.context.quiet:
|
||||
raise LoginRequiredException("Quiet mode requires given password or valid session file.")
|
||||
raise InvalidArgumentException("Quiet mode requires given password or valid session file.")
|
||||
try:
|
||||
password = None
|
||||
while password is None:
|
||||
@@ -1605,3 +1659,10 @@ class Instaloader:
|
||||
except BadCredentialsException as err:
|
||||
print(err, file=sys.stderr)
|
||||
pass
|
||||
|
||||
@property
|
||||
def has_stored_errors(self) -> bool:
|
||||
"""Returns whether any error has been reported and stored to be repeated at program termination.
|
||||
|
||||
.. versionadded: 4.12"""
|
||||
return self.context.has_stored_errors
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
@@ -9,7 +8,7 @@ import textwrap
|
||||
import time
|
||||
import urllib.parse
|
||||
import uuid
|
||||
from contextlib import contextmanager
|
||||
from contextlib import contextmanager, suppress
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
from typing import Any, Callable, Dict, Iterator, List, Optional, Union
|
||||
@@ -33,7 +32,7 @@ def copy_session(session: requests.Session, request_timeout: Optional[float] = N
|
||||
|
||||
def default_user_agent() -> str:
|
||||
return ('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
|
||||
'(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36')
|
||||
'(KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36')
|
||||
|
||||
|
||||
def default_iphone_headers() -> Dict[str, Any]:
|
||||
@@ -94,7 +93,6 @@ class InstaloaderContext:
|
||||
self.quiet = quiet
|
||||
self.max_connection_attempts = max_connection_attempts
|
||||
self._graphql_page_length = 50
|
||||
self._root_rhx_gis = None
|
||||
self.two_factor_auth_pending = None
|
||||
self.iphone_support = iphone_support
|
||||
self.iphone_headers = default_iphone_headers()
|
||||
@@ -104,7 +102,7 @@ class InstaloaderContext:
|
||||
|
||||
self._rate_controller = rate_controller(self) if rate_controller is not None else RateController(self)
|
||||
|
||||
# Can be set to True for testing, disables supression of InstaloaderContext._error_catcher
|
||||
# Can be set to True for testing, disables suppression of InstaloaderContext._error_catcher
|
||||
self.raise_all_errors = False
|
||||
|
||||
# HTTP status codes that should cause an AbortDownloadException
|
||||
@@ -151,6 +149,13 @@ class InstaloaderContext:
|
||||
if repeat_at_end:
|
||||
self.error_log.append(msg)
|
||||
|
||||
@property
|
||||
def has_stored_errors(self) -> bool:
|
||||
"""Returns whether any error has been reported and stored to be repeated at program termination.
|
||||
|
||||
.. versionadded: 4.12"""
|
||||
return bool(self.error_log)
|
||||
|
||||
def close(self):
|
||||
"""Print error log and close session"""
|
||||
if self.error_log and not self.quiet:
|
||||
@@ -236,17 +241,26 @@ class InstaloaderContext:
|
||||
|
||||
def test_login(self) -> Optional[str]:
|
||||
"""Not meant to be used directly, use :meth:`Instaloader.test_login`."""
|
||||
data = self.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {})
|
||||
return data["data"]["user"]["username"] if data["data"]["user"] is not None else None
|
||||
try:
|
||||
data = self.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {})
|
||||
return data["data"]["user"]["username"] if data["data"]["user"] is not None else None
|
||||
except (AbortDownloadException, ConnectionException) as err:
|
||||
self.error(f"Error when checking if logged in: {err}")
|
||||
return None
|
||||
|
||||
def login(self, user, passwd):
|
||||
"""Not meant to be used directly, use :meth:`Instaloader.login`.
|
||||
|
||||
:raises InvalidArgumentException: If the provided username does not exist.
|
||||
:raises BadCredentialsException: If the provided password is wrong.
|
||||
:raises ConnectionException: If connection to Instagram failed.
|
||||
:raises TwoFactorAuthRequiredException: First step of 2FA login done, now call
|
||||
:meth:`Instaloader.two_factor_login`."""
|
||||
:meth:`Instaloader.two_factor_login`.
|
||||
:raises LoginException: An error happened during login (for example, and invalid response).
|
||||
Or if the provided username does not exist.
|
||||
|
||||
.. versionchanged:: 4.12
|
||||
Raises LoginException instead of ConnectionException when an error happens.
|
||||
Raises LoginException instead of InvalidArgumentException when the username does not exist.
|
||||
"""
|
||||
# pylint:disable=import-outside-toplevel
|
||||
import http.client
|
||||
# pylint:disable=protected-access
|
||||
@@ -277,7 +291,7 @@ class InstaloaderContext:
|
||||
resp_json = login.json()
|
||||
|
||||
except json.decoder.JSONDecodeError as err:
|
||||
raise ConnectionException(
|
||||
raise LoginException(
|
||||
"Login error: JSON decode fail, {} - {}.".format(login.status_code, login.reason)
|
||||
) from err
|
||||
if resp_json.get('two_factor_required'):
|
||||
@@ -289,31 +303,32 @@ class InstaloaderContext:
|
||||
resp_json['two_factor_info']['two_factor_identifier'])
|
||||
raise TwoFactorAuthRequiredException("Login error: two-factor authentication required.")
|
||||
if resp_json.get('checkpoint_url'):
|
||||
raise ConnectionException("Login: Checkpoint required. Point your browser to "
|
||||
"https://www.instagram.com{} - "
|
||||
"follow the instructions, then retry.".format(resp_json.get('checkpoint_url')))
|
||||
raise LoginException(
|
||||
f"Login: Checkpoint required. Point your browser to {resp_json.get('checkpoint_url')} - "
|
||||
f"follow the instructions, then retry."
|
||||
)
|
||||
if resp_json['status'] != 'ok':
|
||||
if 'message' in resp_json:
|
||||
raise ConnectionException("Login error: \"{}\" status, message \"{}\".".format(resp_json['status'],
|
||||
resp_json['message']))
|
||||
raise LoginException("Login error: \"{}\" status, message \"{}\".".format(resp_json['status'],
|
||||
resp_json['message']))
|
||||
else:
|
||||
raise ConnectionException("Login error: \"{}\" status.".format(resp_json['status']))
|
||||
raise LoginException("Login error: \"{}\" status.".format(resp_json['status']))
|
||||
if 'authenticated' not in resp_json:
|
||||
# Issue #472
|
||||
if 'message' in resp_json:
|
||||
raise ConnectionException("Login error: Unexpected response, \"{}\".".format(resp_json['message']))
|
||||
raise LoginException("Login error: Unexpected response, \"{}\".".format(resp_json['message']))
|
||||
else:
|
||||
raise ConnectionException("Login error: Unexpected response, this might indicate a blocked IP.")
|
||||
raise LoginException("Login error: Unexpected response, this might indicate a blocked IP.")
|
||||
if not resp_json['authenticated']:
|
||||
if resp_json['user']:
|
||||
# '{"authenticated": false, "user": true, "status": "ok"}'
|
||||
raise BadCredentialsException('Login error: Wrong password.')
|
||||
else:
|
||||
# '{"authenticated": false, "user": false, "status": "ok"}'
|
||||
# Raise InvalidArgumentException rather than BadCredentialException, because BadCredentialException
|
||||
# Raise LoginException rather than BadCredentialException, because BadCredentialException
|
||||
# triggers re-asking of password in Instaloader.interactive_login(), which makes no sense if the
|
||||
# username is invalid.
|
||||
raise InvalidArgumentException('Login error: User {} does not exist.'.format(user))
|
||||
raise LoginException('Login error: User {} does not exist.'.format(user))
|
||||
# '{"authenticated": true, "user": true, "userId": ..., "oneTapPrompt": false, "status": "ok"}'
|
||||
session.headers.update({'X-CSRFToken': login.cookies['csrftoken']})
|
||||
self._session = session
|
||||
@@ -351,33 +366,61 @@ class InstaloaderContext:
|
||||
if self.sleep:
|
||||
time.sleep(min(random.expovariate(0.6), 15.0))
|
||||
|
||||
@staticmethod
|
||||
def _response_error(resp: requests.Response) -> str:
|
||||
extra_from_json: Optional[str] = None
|
||||
with suppress(json.decoder.JSONDecodeError):
|
||||
resp_json = resp.json()
|
||||
if "status" in resp_json:
|
||||
extra_from_json = (
|
||||
f"\"{resp_json['status']}\" status, message \"{resp_json['message']}\""
|
||||
if "message" in resp_json
|
||||
else f"\"{resp_json['status']}\" status"
|
||||
)
|
||||
return (
|
||||
f"{resp.status_code} {resp.reason}"
|
||||
f"{f' - {extra_from_json}' if extra_from_json is not None else ''}"
|
||||
f" when accessing {resp.url}"
|
||||
)
|
||||
|
||||
def get_json(self, path: str, params: Dict[str, Any], host: str = 'www.instagram.com',
|
||||
session: Optional[requests.Session] = None, _attempt=1,
|
||||
response_headers: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
response_headers: Optional[Dict[str, Any]] = None,
|
||||
use_post: bool = False) -> Dict[str, Any]:
|
||||
"""JSON request to Instagram.
|
||||
|
||||
:param path: URL, relative to the given domain which defaults to www.instagram.com/
|
||||
:param params: GET parameters
|
||||
:param params: request parameters
|
||||
:param host: Domain part of the URL from where to download the requested JSON; defaults to www.instagram.com
|
||||
:param session: Session to use, or None to use self.session
|
||||
:param use_post: Use POST instead of GET to make the request
|
||||
:return: Decoded response dictionary
|
||||
:raises QueryReturnedBadRequestException: When the server responds with a 400.
|
||||
:raises QueryReturnedNotFoundException: When the server responds with a 404.
|
||||
:raises ConnectionException: When query repeatedly failed.
|
||||
|
||||
.. versionchanged:: 4.13
|
||||
Added `use_post` parameter.
|
||||
"""
|
||||
is_graphql_query = 'query_hash' in params and 'graphql/query' in path
|
||||
is_doc_id_query = 'doc_id' in params and 'graphql/query' in path
|
||||
is_iphone_query = host == 'i.instagram.com'
|
||||
is_other_query = not is_graphql_query and host == "www.instagram.com"
|
||||
is_other_query = not is_graphql_query and not is_doc_id_query and host == "www.instagram.com"
|
||||
sess = session if session else self._session
|
||||
try:
|
||||
self.do_sleep()
|
||||
if is_graphql_query:
|
||||
self._rate_controller.wait_before_query(params['query_hash'])
|
||||
if is_doc_id_query:
|
||||
self._rate_controller.wait_before_query(params['doc_id'])
|
||||
if is_iphone_query:
|
||||
self._rate_controller.wait_before_query('iphone')
|
||||
if is_other_query:
|
||||
self._rate_controller.wait_before_query('other')
|
||||
resp = sess.get('https://{0}/{1}'.format(host, path), params=params, allow_redirects=False)
|
||||
if use_post:
|
||||
resp = sess.post('https://{0}/{1}'.format(host, path), data=params, allow_redirects=False)
|
||||
else:
|
||||
resp = sess.get('https://{0}/{1}'.format(host, path), params=params, allow_redirects=False)
|
||||
if resp.status_code in self.fatal_status_codes:
|
||||
redirect = " redirect to {}".format(resp.headers['location']) if 'location' in resp.headers else ""
|
||||
body = ""
|
||||
@@ -392,7 +435,7 @@ class InstaloaderContext:
|
||||
if (redirect_url.startswith('https://www.instagram.com/accounts/login') or
|
||||
redirect_url.startswith('https://i.instagram.com/accounts/login')):
|
||||
if not self.is_logged_in:
|
||||
raise LoginRequiredException("Redirected to login page. Use --login.")
|
||||
raise LoginRequiredException("Redirected to login page. Use --login or --load-cookies.")
|
||||
raise AbortDownloadException("Redirected to login page. You've been logged out, please wait " +
|
||||
"some time, recreate the session and try again")
|
||||
if redirect_url.startswith('https://{}/'.format(host)):
|
||||
@@ -404,21 +447,17 @@ class InstaloaderContext:
|
||||
response_headers.clear()
|
||||
response_headers.update(resp.headers)
|
||||
if resp.status_code == 400:
|
||||
raise QueryReturnedBadRequestException("400 Bad Request")
|
||||
raise QueryReturnedBadRequestException(self._response_error(resp))
|
||||
if resp.status_code == 404:
|
||||
raise QueryReturnedNotFoundException("404 Not Found")
|
||||
raise QueryReturnedNotFoundException(self._response_error(resp))
|
||||
if resp.status_code == 429:
|
||||
raise TooManyRequestsException("429 Too Many Requests")
|
||||
raise TooManyRequestsException(self._response_error(resp))
|
||||
if resp.status_code != 200:
|
||||
raise ConnectionException("HTTP error code {}.".format(resp.status_code))
|
||||
raise ConnectionException(self._response_error(resp))
|
||||
else:
|
||||
resp_json = resp.json()
|
||||
if 'status' in resp_json and resp_json['status'] != "ok":
|
||||
if 'message' in resp_json:
|
||||
raise ConnectionException("Returned \"{}\" status, message \"{}\".".format(resp_json['status'],
|
||||
resp_json['message']))
|
||||
else:
|
||||
raise ConnectionException("Returned \"{}\" status.".format(resp_json['status']))
|
||||
raise ConnectionException(self._response_error(resp))
|
||||
return resp_json
|
||||
except (ConnectionException, json.decoder.JSONDecodeError, requests.exceptions.RequestException) as err:
|
||||
error_string = "JSON Query to {}: {}".format(path, err)
|
||||
@@ -432,6 +471,8 @@ class InstaloaderContext:
|
||||
if isinstance(err, TooManyRequestsException):
|
||||
if is_graphql_query:
|
||||
self._rate_controller.handle_429(params['query_hash'])
|
||||
if is_doc_id_query:
|
||||
self._rate_controller.handle_429(params['doc_id'])
|
||||
if is_iphone_query:
|
||||
self._rate_controller.handle_429('iphone')
|
||||
if is_other_query:
|
||||
@@ -443,14 +484,48 @@ class InstaloaderContext:
|
||||
raise ConnectionException(error_string) from err
|
||||
|
||||
def graphql_query(self, query_hash: str, variables: Dict[str, Any],
|
||||
referer: Optional[str] = None, rhx_gis: Optional[str] = None) -> Dict[str, Any]:
|
||||
referer: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Do a GraphQL Query.
|
||||
|
||||
:param query_hash: Query identifying hash.
|
||||
:param variables: Variables for the Query.
|
||||
:param referer: HTTP Referer, or None.
|
||||
:param rhx_gis: 'rhx_gis' variable as somewhere returned by Instagram, needed to 'sign' request
|
||||
:return: The server's response dictionary.
|
||||
|
||||
.. versionchanged:: 4.13.1
|
||||
Removed the `rhx_gis` parameter.
|
||||
"""
|
||||
with copy_session(self._session, self.request_timeout) as tmpsession:
|
||||
tmpsession.headers.update(self._default_http_header(empty_session_only=True))
|
||||
del tmpsession.headers['Connection']
|
||||
del tmpsession.headers['Content-Length']
|
||||
tmpsession.headers['authority'] = 'www.instagram.com'
|
||||
tmpsession.headers['scheme'] = 'https'
|
||||
tmpsession.headers['accept'] = '*/*'
|
||||
if referer is not None:
|
||||
tmpsession.headers['referer'] = urllib.parse.quote(referer)
|
||||
|
||||
variables_json = json.dumps(variables, separators=(',', ':'))
|
||||
|
||||
resp_json = self.get_json('graphql/query',
|
||||
params={'query_hash': query_hash,
|
||||
'variables': variables_json},
|
||||
session=tmpsession)
|
||||
if 'status' not in resp_json:
|
||||
self.error("GraphQL response did not contain a \"status\" field.")
|
||||
return resp_json
|
||||
|
||||
def doc_id_graphql_query(self, doc_id: str, variables: Dict[str, Any],
|
||||
referer: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
Do a doc_id-based GraphQL Query using method POST.
|
||||
|
||||
.. versionadded:: 4.13
|
||||
|
||||
:param doc_id: doc_id for the query.
|
||||
:param variables: Variables for the Query.
|
||||
:param referer: HTTP Referer, or None.
|
||||
:return: The server's response dictionary.
|
||||
"""
|
||||
with copy_session(self._session, self.request_timeout) as tmpsession:
|
||||
@@ -465,16 +540,12 @@ class InstaloaderContext:
|
||||
|
||||
variables_json = json.dumps(variables, separators=(',', ':'))
|
||||
|
||||
if rhx_gis:
|
||||
#self.log("rhx_gis {} query_hash {}".format(rhx_gis, query_hash))
|
||||
values = "{}:{}".format(rhx_gis, variables_json)
|
||||
x_instagram_gis = hashlib.md5(values.encode()).hexdigest()
|
||||
tmpsession.headers['x-instagram-gis'] = x_instagram_gis
|
||||
|
||||
resp_json = self.get_json('graphql/query',
|
||||
params={'query_hash': query_hash,
|
||||
'variables': variables_json},
|
||||
session=tmpsession)
|
||||
params={'variables': variables_json,
|
||||
'doc_id': doc_id,
|
||||
'server_timestamps': 'true'},
|
||||
session=tmpsession,
|
||||
use_post=True)
|
||||
if 'status' not in resp_json:
|
||||
self.error("GraphQL response did not contain a \"status\" field.")
|
||||
return resp_json
|
||||
@@ -482,7 +553,7 @@ class InstaloaderContext:
|
||||
def graphql_node_list(self, query_hash: str, query_variables: Dict[str, Any],
|
||||
query_referer: Optional[str],
|
||||
edge_extractor: Callable[[Dict[str, Any]], Dict[str, Any]],
|
||||
rhx_gis: Optional[str] = None,
|
||||
_rhx_gis: Optional[str] = None,
|
||||
first_data: Optional[Dict[str, Any]] = None) -> Iterator[Dict[str, Any]]:
|
||||
"""
|
||||
Retrieve a list of GraphQL nodes.
|
||||
@@ -494,7 +565,7 @@ class InstaloaderContext:
|
||||
def _query():
|
||||
query_variables['first'] = self._graphql_page_length
|
||||
try:
|
||||
return edge_extractor(self.graphql_query(query_hash, query_variables, query_referer, rhx_gis))
|
||||
return edge_extractor(self.graphql_query(query_hash, query_variables, query_referer))
|
||||
except QueryReturnedBadRequestException:
|
||||
new_page_length = int(self._graphql_page_length / 2)
|
||||
if new_page_length >= 12:
|
||||
@@ -607,11 +678,11 @@ class InstaloaderContext:
|
||||
else:
|
||||
if resp.status_code == 403:
|
||||
# suspected invalid URL signature
|
||||
raise QueryReturnedForbiddenException("403 when accessing {}.".format(url))
|
||||
raise QueryReturnedForbiddenException(self._response_error(resp))
|
||||
if resp.status_code == 404:
|
||||
# 404 not worth retrying.
|
||||
raise QueryReturnedNotFoundException("404 when accessing {}.".format(url))
|
||||
raise ConnectionException("HTTP error code {}.".format(resp.status_code))
|
||||
raise QueryReturnedNotFoundException(self._response_error(resp))
|
||||
raise ConnectionException(self._response_error(resp))
|
||||
|
||||
def get_and_write_raw(self, url: str, filename: str) -> None:
|
||||
"""Downloads and writes anonymously-requested raw data into a file.
|
||||
@@ -637,22 +708,11 @@ class InstaloaderContext:
|
||||
else:
|
||||
if resp.status_code == 403:
|
||||
# suspected invalid URL signature
|
||||
raise QueryReturnedForbiddenException("403 when accessing {}.".format(url))
|
||||
raise QueryReturnedForbiddenException(self._response_error(resp))
|
||||
if resp.status_code == 404:
|
||||
# 404 not worth retrying.
|
||||
raise QueryReturnedNotFoundException("404 when accessing {}.".format(url))
|
||||
raise ConnectionException("HTTP error code {}.".format(resp.status_code))
|
||||
|
||||
@property
|
||||
def root_rhx_gis(self) -> Optional[str]:
|
||||
"""rhx_gis string returned in the / query."""
|
||||
if self.is_logged_in:
|
||||
# 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.
|
||||
return None
|
||||
if self._root_rhx_gis is None:
|
||||
self._root_rhx_gis = self.get_json('', {}).get('rhx_gis', '')
|
||||
return self._root_rhx_gis or None
|
||||
raise QueryReturnedNotFoundException(self._response_error(resp))
|
||||
raise ConnectionException(self._response_error(resp))
|
||||
|
||||
|
||||
class RateController:
|
||||
|
||||
@@ -18,6 +18,7 @@ class LatestStamps:
|
||||
POST_TIMESTAMP = 'post-timestamp'
|
||||
TAGGED_TIMESTAMP = 'tagged-timestamp'
|
||||
IGTV_TIMESTAMP = 'igtv-timestamp'
|
||||
REELS_TIMESTAMP = 'reels-timestamp'
|
||||
STORY_TIMESTAMP = 'story-timestamp'
|
||||
ISO_FORMAT = '%Y-%m-%dT%H:%M:%S.%f%z'
|
||||
|
||||
@@ -87,6 +88,14 @@ class LatestStamps:
|
||||
"""Sets timestamp of last download of a profile's tagged posts."""
|
||||
self._set_timestamp(profile_name, self.TAGGED_TIMESTAMP, timestamp)
|
||||
|
||||
def get_last_reels_timestamp(self, profile_name: str) -> datetime:
|
||||
"""Returns timestamp of last download of a profile's reels posts."""
|
||||
return self._get_timestamp(profile_name, self.REELS_TIMESTAMP)
|
||||
|
||||
def set_last_reels_timestamp(self, profile_name: str, timestamp: datetime):
|
||||
"""Sets timestamp of last download of a profile's reels posts."""
|
||||
self._set_timestamp(profile_name, self.REELS_TIMESTAMP, timestamp)
|
||||
|
||||
def get_last_igtv_timestamp(self, profile_name: str) -> datetime:
|
||||
"""Returns timestamp of last download of a profile's igtv posts."""
|
||||
return self._get_timestamp(profile_name, self.IGTV_TIMESTAMP)
|
||||
|
||||
+43
-22
@@ -7,11 +7,11 @@ from datetime import datetime, timedelta
|
||||
from lzma import LZMAError
|
||||
from typing import Any, Callable, Dict, Iterable, Iterator, NamedTuple, Optional, Tuple, TypeVar
|
||||
|
||||
from .exceptions import AbortDownloadException, InvalidArgumentException, QueryReturnedBadRequestException
|
||||
from .exceptions import AbortDownloadException, InvalidArgumentException
|
||||
from .instaloadercontext import InstaloaderContext
|
||||
|
||||
class FrozenNodeIterator(NamedTuple):
|
||||
query_hash: str
|
||||
query_hash: Optional[str]
|
||||
query_variables: Dict
|
||||
query_referer: Optional[str]
|
||||
context_username: Optional[str]
|
||||
@@ -19,6 +19,7 @@ class FrozenNodeIterator(NamedTuple):
|
||||
best_before: Optional[float]
|
||||
remaining_data: Optional[Dict]
|
||||
first_node: Optional[Dict]
|
||||
doc_id: Optional[str]
|
||||
FrozenNodeIterator.query_hash.__doc__ = """The GraphQL ``query_hash`` parameter."""
|
||||
FrozenNodeIterator.query_variables.__doc__ = """The GraphQL ``query_variables`` parameter."""
|
||||
FrozenNodeIterator.query_referer.__doc__ = """The HTTP referer used for the GraphQL query."""
|
||||
@@ -28,6 +29,7 @@ FrozenNodeIterator.best_before.__doc__ = """Date when parts of the stored nodes
|
||||
FrozenNodeIterator.remaining_data.__doc__ = \
|
||||
"""The already-retrieved, yet-unprocessed ``edges`` and the ``page_info`` at time of freezing."""
|
||||
FrozenNodeIterator.first_node.__doc__ = """Node data of the first item, if an item has been produced."""
|
||||
FrozenNodeIterator.doc_id.__doc__ = """The GraphQL ``doc_id`` parameter."""
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
@@ -64,22 +66,27 @@ class NodeIterator(Iterator[T]):
|
||||
NodeIterators are matching if and only if they have the same magic.
|
||||
|
||||
See also :func:`resumable_iteration` for a high-level context manager that handles a resumable iteration.
|
||||
|
||||
.. versionchanged: 4.13
|
||||
Included support for `doc_id`-based queries (using POST method).
|
||||
"""
|
||||
|
||||
_graphql_page_length = 50
|
||||
_graphql_page_length = 12
|
||||
_shelf_life = timedelta(days=29)
|
||||
|
||||
def __init__(self,
|
||||
context: InstaloaderContext,
|
||||
query_hash: str,
|
||||
query_hash: Optional[str],
|
||||
edge_extractor: Callable[[Dict[str, Any]], Dict[str, Any]],
|
||||
node_wrapper: Callable[[Dict], T],
|
||||
query_variables: Optional[Dict[str, Any]] = None,
|
||||
query_referer: Optional[str] = None,
|
||||
first_data: Optional[Dict[str, Any]] = None,
|
||||
is_first: Optional[Callable[[T, Optional[T]], bool]] = None):
|
||||
is_first: Optional[Callable[[T, Optional[T]], bool]] = None,
|
||||
doc_id: Optional[str] = None):
|
||||
self._context = context
|
||||
self._query_hash = query_hash
|
||||
self._doc_id = doc_id
|
||||
self._edge_extractor = edge_extractor
|
||||
self._node_wrapper = node_wrapper
|
||||
self._query_variables = query_variables if query_variables is not None else {}
|
||||
@@ -95,26 +102,38 @@ class NodeIterator(Iterator[T]):
|
||||
self._is_first = is_first
|
||||
|
||||
def _query(self, after: Optional[str] = None) -> Dict:
|
||||
if self._doc_id is not None:
|
||||
return self._query_doc_id(self._doc_id, after)
|
||||
else:
|
||||
assert self._query_hash is not None
|
||||
return self._query_query_hash(self._query_hash, after)
|
||||
|
||||
def _query_doc_id(self, doc_id: str, after: Optional[str] = None) -> Dict:
|
||||
pagination_variables: Dict[str, Any] = {'__relay_internal__pv__PolarisFeedShareMenurelayprovider': False}
|
||||
if after is not None:
|
||||
pagination_variables['after'] = after
|
||||
pagination_variables['before'] = None
|
||||
pagination_variables['first'] = 12
|
||||
pagination_variables['last'] = None
|
||||
data = self._edge_extractor(
|
||||
self._context.doc_id_graphql_query(
|
||||
doc_id, {**self._query_variables, **pagination_variables}, self._query_referer
|
||||
)
|
||||
)
|
||||
self._best_before = datetime.now() + NodeIterator._shelf_life
|
||||
return data
|
||||
|
||||
def _query_query_hash(self, query_hash: str, after: Optional[str] = None) -> Dict:
|
||||
pagination_variables: Dict[str, Any] = {'first': NodeIterator._graphql_page_length}
|
||||
if after is not None:
|
||||
pagination_variables['after'] = after
|
||||
try:
|
||||
data = self._edge_extractor(
|
||||
self._context.graphql_query(
|
||||
self._query_hash, {**self._query_variables, **pagination_variables}, self._query_referer
|
||||
)
|
||||
data = self._edge_extractor(
|
||||
self._context.graphql_query(
|
||||
query_hash, {**self._query_variables, **pagination_variables}, self._query_referer
|
||||
)
|
||||
self._best_before = datetime.now() + NodeIterator._shelf_life
|
||||
return data
|
||||
except QueryReturnedBadRequestException:
|
||||
new_page_length = int(NodeIterator._graphql_page_length / 2)
|
||||
if new_page_length >= 12:
|
||||
NodeIterator._graphql_page_length = new_page_length
|
||||
self._context.error("HTTP Error 400 (Bad Request) on GraphQL Query. Retrying with shorter page length.",
|
||||
repeat_at_end=False)
|
||||
return self._query(after)
|
||||
else:
|
||||
raise
|
||||
)
|
||||
self._best_before = datetime.now() + NodeIterator._shelf_life
|
||||
return data
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
@@ -203,6 +222,7 @@ class NodeIterator(Iterator[T]):
|
||||
best_before=self._best_before.timestamp() if self._best_before else None,
|
||||
remaining_data=remaining_data,
|
||||
first_node=self._first_node,
|
||||
doc_id=self._doc_id,
|
||||
)
|
||||
|
||||
def thaw(self, frozen: FrozenNodeIterator) -> None:
|
||||
@@ -220,7 +240,8 @@ class NodeIterator(Iterator[T]):
|
||||
if (self._query_hash != frozen.query_hash or
|
||||
self._query_variables != frozen.query_variables or
|
||||
self._query_referer != frozen.query_referer or
|
||||
self._context.username != frozen.context_username):
|
||||
self._context.username != frozen.context_username or
|
||||
self._doc_id != frozen.doc_id):
|
||||
raise InvalidArgumentException("Mismatching resume information.")
|
||||
if not frozen.best_before:
|
||||
raise InvalidArgumentException("\"best before\" date missing.")
|
||||
|
||||
+102
-46
@@ -180,7 +180,7 @@ class Post:
|
||||
This class unifies access to the properties associated with a post. It implements == and is
|
||||
hashable.
|
||||
|
||||
:param context: :attr:`Instaloader.context` used for additional queries if neccessary..
|
||||
:param context: :attr:`Instaloader.context` used for additional queries if necessary..
|
||||
:param node: Node structure, as returned by Instagram.
|
||||
:param owner_profile: The Profile of the owner, if already known at creation.
|
||||
"""
|
||||
@@ -232,21 +232,32 @@ class Post:
|
||||
"title": media.get("title"),
|
||||
"viewer_has_liked": media["has_liked"],
|
||||
"edge_media_preview_like": {"count": media["like_count"]},
|
||||
"accessibility_caption": media.get("accessibility_caption"),
|
||||
"comments": media.get("comment_count"),
|
||||
"iphone_struct": media,
|
||||
}
|
||||
with suppress(KeyError):
|
||||
fake_node["display_url"] = media['image_versions2']['candidates'][0]['url']
|
||||
with suppress(KeyError):
|
||||
with suppress(KeyError, TypeError):
|
||||
fake_node["video_url"] = media['video_versions'][-1]['url']
|
||||
fake_node["video_duration"] = media["video_duration"]
|
||||
fake_node["video_view_count"] = media["view_count"]
|
||||
with suppress(KeyError):
|
||||
fake_node["edge_sidecar_to_children"] = {"edges": [{"node": {
|
||||
"display_url": node['image_versions2']['candidates'][0]['url'],
|
||||
"is_video": media_types[node["media_type"]] == "GraphVideo",
|
||||
}} for node in media["carousel_media"]]}
|
||||
with suppress(KeyError, TypeError):
|
||||
fake_node["edge_sidecar_to_children"] = {"edges": [{"node":
|
||||
Post._convert_iphone_carousel(node, media_types)}
|
||||
for node in media["carousel_media"]]}
|
||||
return cls(context, fake_node, Profile.from_iphone_struct(context, media["user"]) if "user" in media else None)
|
||||
|
||||
@staticmethod
|
||||
def _convert_iphone_carousel(iphone_node: Dict[str, Any], media_types: Dict[int, str]) -> Dict[str, Any]:
|
||||
fake_node = {
|
||||
"display_url": iphone_node["image_versions2"]["candidates"][0]["url"],
|
||||
"is_video": media_types[iphone_node["media_type"]] == "GraphVideo",
|
||||
}
|
||||
if "video_versions" in iphone_node and iphone_node["video_versions"] is not None:
|
||||
fake_node["video_url"] = iphone_node["video_versions"][0]["url"]
|
||||
return fake_node
|
||||
|
||||
@staticmethod
|
||||
def shortcode_to_mediaid(code: str) -> int:
|
||||
if len(code) > 11:
|
||||
@@ -308,13 +319,23 @@ class Post:
|
||||
|
||||
def _obtain_metadata(self):
|
||||
if not self._full_metadata_dict:
|
||||
pic_json = self._context.graphql_query(
|
||||
'2b0673e0dc4580674a88d426fe00ea90',
|
||||
{'shortcode': self.shortcode}
|
||||
)
|
||||
self._full_metadata_dict = pic_json['data']['shortcode_media']
|
||||
if self._full_metadata_dict is None:
|
||||
pic_json = self._context.doc_id_graphql_query(
|
||||
"8845758582119845", {"shortcode": self.shortcode}
|
||||
)["data"]["xdt_shortcode_media"]
|
||||
if pic_json is None:
|
||||
raise BadResponseException("Fetching Post metadata failed.")
|
||||
try:
|
||||
xdt_types = {
|
||||
"XDTGraphImage": "GraphImage",
|
||||
"XDTGraphVideo": "GraphVideo",
|
||||
"XDTGraphSidecar": "GraphSidecar",
|
||||
}
|
||||
pic_json["__typename"] = xdt_types[pic_json["__typename"]]
|
||||
except KeyError as exc:
|
||||
raise BadResponseException(
|
||||
f"Unknown __typename in metadata: {pic_json['__typename']}."
|
||||
) from exc
|
||||
self._full_metadata_dict = pic_json
|
||||
if self.shortcode != self._full_metadata_dict['shortcode']:
|
||||
self._node.update(self._full_metadata_dict)
|
||||
raise PostChangedException
|
||||
@@ -330,7 +351,7 @@ class Post:
|
||||
if not self._context.iphone_support:
|
||||
raise IPhoneSupportDisabledException("iPhone support is disabled.")
|
||||
if not self._context.is_logged_in:
|
||||
raise LoginRequiredException("--login required to access iPhone media info endpoint.")
|
||||
raise LoginRequiredException("Login required to access iPhone media info endpoint.")
|
||||
if not self._iphone_struct_:
|
||||
data = self._context.get_iphone_json(path='api/v1/media/{}/info/'.format(self.mediaid), params={})
|
||||
self._iphone_struct_ = data['items'][0]
|
||||
@@ -491,14 +512,14 @@ class Post:
|
||||
|
||||
@property
|
||||
def caption_hashtags(self) -> List[str]:
|
||||
"""List of all lowercased hashtags (without preceeding #) that occur in the Post's caption."""
|
||||
"""List of all lowercased hashtags (without preceding #) that occur in the Post's caption."""
|
||||
if not self.caption:
|
||||
return []
|
||||
return _hashtag_regex.findall(self.caption.lower())
|
||||
|
||||
@property
|
||||
def caption_mentions(self) -> List[str]:
|
||||
"""List of all lowercased profiles that are mentioned in the Post's caption, without preceeding @."""
|
||||
"""List of all lowercased profiles that are mentioned in the Post's caption, without preceding @."""
|
||||
if not self.caption:
|
||||
return []
|
||||
return _mention_regex.findall(self.caption.lower())
|
||||
@@ -685,6 +706,9 @@ class Post:
|
||||
.. versionchanged:: 4.7
|
||||
Change return type to ``Iterable``.
|
||||
"""
|
||||
if not self._context.is_logged_in:
|
||||
raise LoginRequiredException("Login required to access comments of a post.")
|
||||
|
||||
def _postcommentanswer(node):
|
||||
return PostCommentAnswer(id=int(node['id']),
|
||||
created_at_utc=datetime.utcfromtimestamp(node['created_at']),
|
||||
@@ -720,7 +744,11 @@ class Post:
|
||||
# Avoid doing additional requests if there are no comments
|
||||
return []
|
||||
|
||||
comment_edges = self._field('edge_media_to_comment', 'edges')
|
||||
try:
|
||||
comment_edges = self._field("edge_media_to_parent_comment", "edges")
|
||||
except KeyError:
|
||||
comment_edges = self._field("edge_media_to_comment", "edges")
|
||||
|
||||
answers_count = sum(edge['node'].get('edge_threaded_comments', {}).get('count', 0) for edge in comment_edges)
|
||||
|
||||
if self.comments == len(comment_edges) + answers_count:
|
||||
@@ -749,7 +777,7 @@ class Post:
|
||||
Require being logged in (as required by Instagram).
|
||||
"""
|
||||
if not self._context.is_logged_in:
|
||||
raise LoginRequiredException("--login required to access likes of a post.")
|
||||
raise LoginRequiredException("Login required to access likes of a post.")
|
||||
if self.likes == 0:
|
||||
# Avoid doing additional requests if there are no comments
|
||||
return
|
||||
@@ -894,8 +922,7 @@ class Profile:
|
||||
'include_reel': True,
|
||||
'include_suggested_users': False,
|
||||
'include_logged_out_extras': False,
|
||||
'include_highlight_reels': False},
|
||||
rhx_gis=context.root_rhx_gis)['data']['user']
|
||||
'include_highlight_reels': False})['data']['user']
|
||||
if data:
|
||||
profile = cls(context, data['reel']['owner'])
|
||||
else:
|
||||
@@ -926,7 +953,7 @@ class Profile:
|
||||
|
||||
.. versionadded:: 4.5.2"""
|
||||
if not context.is_logged_in:
|
||||
raise LoginRequiredException("--login required to access own profile.")
|
||||
raise LoginRequiredException("Login required to access own profile.")
|
||||
return cls(context, context.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {})["data"]["user"])
|
||||
|
||||
def _asdict(self):
|
||||
@@ -980,7 +1007,7 @@ class Profile:
|
||||
if not self._context.iphone_support:
|
||||
raise IPhoneSupportDisabledException("iPhone support is disabled.")
|
||||
if not self._context.is_logged_in:
|
||||
raise LoginRequiredException("--login required to access iPhone profile info endpoint.")
|
||||
raise LoginRequiredException("Login required to access iPhone profile info endpoint.")
|
||||
if not self._iphone_struct_:
|
||||
data = self._context.get_iphone_json(path='api/v1/users/{}/info/'.format(self.userid), params={})
|
||||
self._iphone_struct_ = data['user']
|
||||
@@ -1052,7 +1079,7 @@ class Profile:
|
||||
@property
|
||||
def biography_hashtags(self) -> List[str]:
|
||||
"""
|
||||
List of all lowercased hashtags (without preceeding #) that occur in the Profile's biography.
|
||||
List of all lowercased hashtags (without preceding #) that occur in the Profile's biography.
|
||||
|
||||
.. versionadded:: 4.10
|
||||
"""
|
||||
@@ -1063,7 +1090,7 @@ class Profile:
|
||||
@property
|
||||
def biography_mentions(self) -> List[str]:
|
||||
"""
|
||||
List of all lowercased profiles that are mentioned in the Profile's biography, without preceeding @.
|
||||
List of all lowercased profiles that are mentioned in the Profile's biography, without preceding @.
|
||||
|
||||
.. versionadded:: 4.10
|
||||
"""
|
||||
@@ -1171,14 +1198,17 @@ class Profile:
|
||||
:rtype: NodeIterator[Post]"""
|
||||
self._obtain_metadata()
|
||||
return NodeIterator(
|
||||
self._context,
|
||||
'003056d32c2554def87228bc3fd9668a',
|
||||
lambda d: d['data']['user']['edge_owner_to_timeline_media'],
|
||||
lambda n: Post(self._context, n, self),
|
||||
{'id': self.userid},
|
||||
'https://www.instagram.com/{0}/'.format(self.username),
|
||||
self._metadata('edge_owner_to_timeline_media'),
|
||||
Profile._make_is_newest_checker()
|
||||
context = self._context,
|
||||
edge_extractor = lambda d: d['data']['xdt_api__v1__feed__user_timeline_graphql_connection'],
|
||||
node_wrapper = lambda n: Post.from_iphone_struct(self._context, n),
|
||||
query_variables = {'data': {
|
||||
'count': 12, 'include_relationship_info': True,
|
||||
'latest_besties_reel_media': True, 'latest_reel_media': True},
|
||||
'username': self.username},
|
||||
query_referer = 'https://www.instagram.com/{0}/'.format(self.username),
|
||||
is_first = Profile._make_is_newest_checker(),
|
||||
doc_id = '7898261790222653',
|
||||
query_hash = None,
|
||||
)
|
||||
|
||||
def get_saved_posts(self) -> NodeIterator[Post]:
|
||||
@@ -1187,7 +1217,7 @@ class Profile:
|
||||
:rtype: NodeIterator[Post]"""
|
||||
|
||||
if self.username != self._context.username:
|
||||
raise LoginRequiredException("--login={} required to get that profile's saved posts.".format(self.username))
|
||||
raise LoginRequiredException(f"Login as {self.username} required to get that profile's saved posts.")
|
||||
|
||||
return NodeIterator(
|
||||
self._context,
|
||||
@@ -1215,6 +1245,30 @@ class Profile:
|
||||
is_first=Profile._make_is_newest_checker()
|
||||
)
|
||||
|
||||
def get_reels(self) -> NodeIterator[Post]:
|
||||
"""Retrieve all reels from a profile.
|
||||
|
||||
:rtype: NodeIterator[Post]
|
||||
|
||||
.. versionadded:: 4.14.0
|
||||
|
||||
"""
|
||||
self._obtain_metadata()
|
||||
return NodeIterator(
|
||||
context = self._context,
|
||||
edge_extractor = lambda d: d['data']['xdt_api__v1__clips__user__connection_v2'],
|
||||
# Reels post info is incomplete relative to regular posts so we create a Post from the shortcode
|
||||
# and fetch the additional metadata with an additional API request per Reel
|
||||
node_wrapper = lambda n: Post.from_shortcode(context=self._context, shortcode=n["media"]["code"]),
|
||||
query_variables = {'data': {
|
||||
'page_size': 12, 'include_feed_video': True, "target_user_id": str(self.userid)}},
|
||||
query_referer = 'https://www.instagram.com/{0}/'.format(self.username),
|
||||
is_first = Profile._make_is_newest_checker(),
|
||||
# fb_api_req_friendly_name=PolarisProfileReelsTabContentQuery_connection
|
||||
doc_id = '7845543455542541',
|
||||
query_hash = None,
|
||||
)
|
||||
|
||||
def get_igtv_posts(self) -> NodeIterator[Post]:
|
||||
"""Retrieve all IGTV posts.
|
||||
|
||||
@@ -1247,7 +1301,7 @@ class Profile:
|
||||
.. versionadded:: 4.10
|
||||
"""
|
||||
if not self._context.is_logged_in:
|
||||
raise LoginRequiredException("--login required to get a profile's followers.")
|
||||
raise LoginRequiredException("Login required to get a profile's followers.")
|
||||
self._obtain_metadata()
|
||||
return NodeIterator(
|
||||
self._context,
|
||||
@@ -1266,7 +1320,7 @@ class Profile:
|
||||
:rtype: NodeIterator[Profile]
|
||||
"""
|
||||
if not self._context.is_logged_in:
|
||||
raise LoginRequiredException("--login required to get a profile's followers.")
|
||||
raise LoginRequiredException("Login required to get a profile's followers.")
|
||||
self._obtain_metadata()
|
||||
return NodeIterator(
|
||||
self._context,
|
||||
@@ -1285,7 +1339,7 @@ class Profile:
|
||||
:rtype: NodeIterator[Profile]
|
||||
"""
|
||||
if not self._context.is_logged_in:
|
||||
raise LoginRequiredException("--login required to get a profile's followees.")
|
||||
raise LoginRequiredException("Login required to get a profile's followees.")
|
||||
self._obtain_metadata()
|
||||
return NodeIterator(
|
||||
self._context,
|
||||
@@ -1304,7 +1358,7 @@ class Profile:
|
||||
.. versionadded:: 4.4
|
||||
"""
|
||||
if not self._context.is_logged_in:
|
||||
raise LoginRequiredException("--login required to get a profile's similar accounts.")
|
||||
raise LoginRequiredException("Login required to get a profile's similar accounts.")
|
||||
self._obtain_metadata()
|
||||
yield from (Profile(self._context, edge["node"]) for edge in
|
||||
self._context.graphql_query("ad99dd9d3646cc3c0dda65debcd266a7",
|
||||
@@ -1383,7 +1437,7 @@ class StoryItem:
|
||||
if not self._context.iphone_support:
|
||||
raise IPhoneSupportDisabledException("iPhone support is disabled.")
|
||||
if not self._context.is_logged_in:
|
||||
raise LoginRequiredException("--login required to access iPhone media info endpoint.")
|
||||
raise LoginRequiredException("Login required to access iPhone media info endpoint.")
|
||||
if not self._iphone_struct_:
|
||||
data = self._context.get_iphone_json(
|
||||
path='api/v1/feed/reels_media/?reel_ids={}'.format(self.owner_id), params={}
|
||||
@@ -1480,7 +1534,7 @@ class StoryItem:
|
||||
@property
|
||||
def caption_hashtags(self) -> List[str]:
|
||||
"""
|
||||
List of all lowercased hashtags (without preceeding #) that occur in the StoryItem's caption.
|
||||
List of all lowercased hashtags (without preceding #) that occur in the StoryItem's caption.
|
||||
|
||||
.. versionadded:: 4.10
|
||||
"""
|
||||
@@ -1491,7 +1545,7 @@ class StoryItem:
|
||||
@property
|
||||
def caption_mentions(self) -> List[str]:
|
||||
"""
|
||||
List of all lowercased profiles that are mentioned in the StoryItem's caption, without preceeding @.
|
||||
List of all lowercased profiles that are mentioned in the StoryItem's caption, without preceding @.
|
||||
|
||||
.. versionadded:: 4.10
|
||||
"""
|
||||
@@ -1790,11 +1844,11 @@ class Hashtag:
|
||||
@classmethod
|
||||
def from_name(cls, context: InstaloaderContext, name: str):
|
||||
"""
|
||||
Create a Hashtag instance from a given hashtag name, without preceeding '#'. Raises an Exception if there is no
|
||||
Create a Hashtag instance from a given hashtag name, without preceding '#'. Raises an Exception if there is no
|
||||
hashtag with the given name.
|
||||
|
||||
:param context: :attr:`Instaloader.context`
|
||||
:param name: Hashtag, without preceeding '#'
|
||||
:param name: Hashtag, without preceding '#'
|
||||
:raises: :class:`QueryReturnedNotFoundException`
|
||||
"""
|
||||
# pylint:disable=protected-access
|
||||
@@ -1804,11 +1858,13 @@ class Hashtag:
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Hashtag name lowercased, without preceeding '#'"""
|
||||
"""Hashtag name lowercased, without preceding '#'"""
|
||||
return self._node["name"].lower()
|
||||
|
||||
def _query(self, params):
|
||||
json_response = self._context.get_json("explore/tags/{0}/".format(self.name), params)
|
||||
json_response = self._context.get_iphone_json(
|
||||
"api/v1/tags/web_info/", {**params, "tag_name": self.name}
|
||||
)
|
||||
return json_response["graphql"]["hashtag"] if "graphql" in json_response else json_response["data"]
|
||||
|
||||
def _obtain_metadata(self):
|
||||
@@ -2131,7 +2187,7 @@ def load_structure(context: InstaloaderContext, json_structure: dict) -> JsonExp
|
||||
"""Loads a :class:`Post`, :class:`Profile`, :class:`StoryItem`, :class:`Hashtag` or :class:`FrozenNodeIterator` from
|
||||
a json structure.
|
||||
|
||||
:param context: :attr:`Instaloader.context` linked to the new object, used for additional queries if neccessary.
|
||||
:param context: :attr:`Instaloader.context` linked to the new object, used for additional queries if necessary.
|
||||
:param json_structure: Instaloader JSON structure
|
||||
|
||||
.. versionadded:: 4.8
|
||||
@@ -2161,7 +2217,7 @@ def load_structure_from_file(context: InstaloaderContext, filename: str) -> Json
|
||||
"""Loads a :class:`Post`, :class:`Profile`, :class:`StoryItem`, :class:`Hashtag` or :class:`FrozenNodeIterator` from
|
||||
a '.json' or '.json.xz' file that has been saved by :func:`save_structure_to_file`.
|
||||
|
||||
:param context: :attr:`Instaloader.context` linked to the new object, used for additional queries if neccessary.
|
||||
:param context: :attr:`Instaloader.context` linked to the new object, used for additional queries if necessary.
|
||||
:param filename: Filename, ends in '.json' or '.json.xz'
|
||||
"""
|
||||
compressed = filename.endswith('.xz')
|
||||
|
||||
@@ -18,10 +18,10 @@ def get_version():
|
||||
raise SystemExit("Could not find version string.")
|
||||
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
sys.exit('Instaloader requires Python >= 3.8.')
|
||||
if sys.version_info < (3, 9):
|
||||
sys.exit('Instaloader requires Python >= 3.9.')
|
||||
|
||||
requirements = ['requests>=2.4']
|
||||
requirements = ['requests>=2.25']
|
||||
optional_requirements = {
|
||||
'browser_cookie3': ['browser_cookie3>=0.19.1'],
|
||||
}
|
||||
@@ -46,7 +46,7 @@ setup(
|
||||
'from Instagram.',
|
||||
long_description=open(os.path.join(SRC, 'README.rst')).read(),
|
||||
install_requires=requirements,
|
||||
python_requires='>=3.8',
|
||||
python_requires='>=3.9',
|
||||
extras_require=optional_requirements,
|
||||
entry_points={'console_scripts': ['instaloader=instaloader.__main__:main']},
|
||||
zip_safe=False,
|
||||
@@ -58,11 +58,11 @@ setup(
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: 3.11',
|
||||
'Programming Language :: Python :: 3.12',
|
||||
'Programming Language :: Python :: 3.13',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Topic :: Internet',
|
||||
'Topic :: Multimedia :: Graphics'
|
||||
|
||||
Reference in New Issue
Block a user