mirror of
https://github.com/instaloader/instaloader.git
synced 2026-10-17 06:02:18 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
14f1c3cb82 | ||
|
|
7a36512093 | ||
|
|
f423aa1507 | ||
|
|
1875aede82 | ||
|
|
2cae933d66 | ||
|
|
5509ea7daf | ||
|
|
2b594909bb | ||
|
|
1852447825 | ||
|
|
baaa2c2998 | ||
|
|
47235bf2b8 | ||
|
|
6eea8e6fcd | ||
|
|
5166fe691a | ||
|
|
9fa01509df | ||
|
|
f0420e8a20 | ||
|
|
7cc13ee08e | ||
|
|
bf396f0e6c | ||
|
|
39566735f7 | ||
|
|
fff25d6f4a | ||
|
|
b52547ea1c |
@@ -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.10.3
|
||||
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.10.3
|
||||
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,16 +11,16 @@ 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,4 +6,5 @@ Instaloader is written by
|
||||
- Alexander Graf (@aandergr)
|
||||
- André Koch-Kramer (@Thammus)
|
||||
- Lars Lindqvist (@e5150)
|
||||
- Eduardo M Kalinowksi (@ekalin)
|
||||
- ... and many more, see https://github.com/instaloader/instaloader/graphs/contributors
|
||||
|
||||
Generated
+309
-394
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "4f18067c3e7ea90c715762631e5846b6aecf1023741f936e1ffbc2cb3c5077a2"
|
||||
"sha256": "7a7000ad88c2f698c6e5f6b3eaaf24b66321e6d1743a8e088bae609626a02133"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@@ -24,11 +24,11 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
|
||||
"sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
|
||||
"sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b",
|
||||
"sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2023.7.22"
|
||||
"version": "==2024.7.4"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
@@ -128,117 +128,126 @@
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
|
||||
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
|
||||
"sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc",
|
||||
"sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.4"
|
||||
"version": "==3.7"
|
||||
},
|
||||
"jeepney": {
|
||||
"hashes": [
|
||||
"sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806",
|
||||
"sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"
|
||||
],
|
||||
"markers": "python_version >= '3.7' and 'bsd' in sys_platform or sys_platform == 'linux'",
|
||||
"version": "==0.8.0"
|
||||
},
|
||||
"lz4": {
|
||||
"hashes": [
|
||||
"sha256:0ca83a623c449295bafad745dcd399cea4c55b16b13ed8cfea30963b004016c9",
|
||||
"sha256:0f5614d8229b33d4a97cb527db2a1ac81308c6e796e7bdb5d1309127289f69d5",
|
||||
"sha256:1c4c100d99eed7c08d4e8852dd11e7d1ec47a3340f49e3a96f8dfbba17ffb300",
|
||||
"sha256:1f25eb322eeb24068bb7647cae2b0732b71e5c639e4e4026db57618dcd8279f0",
|
||||
"sha256:200d05777d61ba1ff8d29cb51c534a162ea0b4fe6d3c28be3571a0a48ff36080",
|
||||
"sha256:31d72731c4ac6ebdce57cd9a5cabe0aecba229c4f31ba3e2c64ae52eee3fdb1c",
|
||||
"sha256:3a85b430138882f82f354135b98c320dafb96fc8fe4656573d95ab05de9eb092",
|
||||
"sha256:4931ab28a0d1c133104613e74eec1b8bb1f52403faabe4f47f93008785c0b929",
|
||||
"sha256:4caedeb19e3ede6c7a178968b800f910db6503cb4cb1e9cc9221157572139b49",
|
||||
"sha256:65d5c93f8badacfa0456b660285e394e65023ef8071142e0dcbd4762166e1be0",
|
||||
"sha256:6b50f096a6a25f3b2edca05aa626ce39979d63c3b160687c8c6d50ac3943d0ba",
|
||||
"sha256:7211dc8f636ca625abc3d4fb9ab74e5444b92df4f8d58ec83c8868a2b0ff643d",
|
||||
"sha256:7a9eec24ec7d8c99aab54de91b4a5a149559ed5b3097cf30249b665689b3d402",
|
||||
"sha256:7c2df117def1589fba1327dceee51c5c2176a2b5a7040b45e84185ce0c08b6a3",
|
||||
"sha256:7e2dc1bd88b60fa09b9b37f08553f45dc2b770c52a5996ea52b2b40f25445676",
|
||||
"sha256:83903fe6db92db0be101acedc677aa41a490b561567fe1b3fe68695b2110326c",
|
||||
"sha256:83acfacab3a1a7ab9694333bcb7950fbeb0be21660d236fd09c8337a50817897",
|
||||
"sha256:86480f14a188c37cb1416cdabacfb4e42f7a5eab20a737dac9c4b1c227f3b822",
|
||||
"sha256:867664d9ca9bdfce840ac96d46cd8838c9ae891e859eb98ce82fcdf0e103a947",
|
||||
"sha256:8df16c9a2377bdc01e01e6de5a6e4bbc66ddf007a6b045688e285d7d9d61d1c9",
|
||||
"sha256:8f00a9ba98f6364cadda366ae6469b7b3568c0cced27e16a47ddf6b774169270",
|
||||
"sha256:926b26db87ec8822cf1870efc3d04d06062730ec3279bbbd33ba47a6c0a5c673",
|
||||
"sha256:a6a46889325fd60b8a6b62ffc61588ec500a1883db32cddee9903edfba0b7584",
|
||||
"sha256:a98b61e504fb69f99117b188e60b71e3c94469295571492a6468c1acd63c37ba",
|
||||
"sha256:ad38dc6a7eea6f6b8b642aaa0683253288b0460b70cab3216838747163fb774d",
|
||||
"sha256:b10b77dc2e6b1daa2f11e241141ab8285c42b4ed13a8642495620416279cc5b2",
|
||||
"sha256:d5ea0e788dc7e2311989b78cae7accf75a580827b4d96bbaf06c7e5a03989bd5",
|
||||
"sha256:e05afefc4529e97c08e65ef92432e5f5225c0bb21ad89dee1e06a882f91d7f5e",
|
||||
"sha256:e1431d84a9cfb23e6773e72078ce8e65cad6745816d4cbf9ae67da5ea419acda",
|
||||
"sha256:ec6755cacf83f0c5588d28abb40a1ac1643f2ff2115481089264c7630236618a",
|
||||
"sha256:edc2fb3463d5d9338ccf13eb512aab61937be50aa70734bcf873f2f493801d3b",
|
||||
"sha256:edd8987d8415b5dad25e797043936d91535017237f72fa456601be1479386c92",
|
||||
"sha256:edda4fb109439b7f3f58ed6bede59694bc631c4b69c041112b1b7dc727fffb23",
|
||||
"sha256:f571eab7fec554d3b1db0d666bdc2ad85c81f4b8cb08906c4c59a8cad75e6e22",
|
||||
"sha256:f7c50542b4ddceb74ab4f8b3435327a0861f06257ca501d59067a6a482535a77"
|
||||
"sha256:01fe674ef2889dbb9899d8a67361e0c4a2c833af5aeb37dd505727cf5d2a131e",
|
||||
"sha256:054b4631a355606e99a42396f5db4d22046a3397ffc3269a348ec41eaebd69d2",
|
||||
"sha256:0a136e44a16fc98b1abc404fbabf7f1fada2bdab6a7e970974fb81cf55b636d0",
|
||||
"sha256:0e9c410b11a31dbdc94c05ac3c480cb4b222460faf9231f12538d0074e56c563",
|
||||
"sha256:222a7e35137d7539c9c33bb53fcbb26510c5748779364014235afc62b0ec797f",
|
||||
"sha256:24b3206de56b7a537eda3a8123c644a2b7bf111f0af53bc14bed90ce5562d1aa",
|
||||
"sha256:2b901c7784caac9a1ded4555258207d9e9697e746cc8532129f150ffe1f6ba0d",
|
||||
"sha256:2f7b1839f795315e480fb87d9bc60b186a98e3e5d17203c6e757611ef7dcef61",
|
||||
"sha256:30e8c20b8857adef7be045c65f47ab1e2c4fabba86a9fa9a997d7674a31ea6b6",
|
||||
"sha256:31ea4be9d0059c00b2572d700bf2c1bc82f241f2c3282034a759c9a4d6ca4dc2",
|
||||
"sha256:337cb94488a1b060ef1685187d6ad4ba8bc61d26d631d7ba909ee984ea736be1",
|
||||
"sha256:33c9a6fd20767ccaf70649982f8f3eeb0884035c150c0b818ea660152cf3c809",
|
||||
"sha256:363ab65bf31338eb364062a15f302fc0fab0a49426051429866d71c793c23394",
|
||||
"sha256:43cf03059c0f941b772c8aeb42a0813d68d7081c009542301637e5782f8a33e2",
|
||||
"sha256:56f4fe9c6327adb97406f27a66420b22ce02d71a5c365c48d6b656b4aaeb7775",
|
||||
"sha256:5d35533bf2cee56f38ced91f766cd0038b6abf46f438a80d50c52750088be93f",
|
||||
"sha256:6756212507405f270b66b3ff7f564618de0606395c0fe10a7ae2ffcbbe0b1fba",
|
||||
"sha256:6cdc60e21ec70266947a48839b437d46025076eb4b12c76bd47f8e5eb8a75dcc",
|
||||
"sha256:abc197e4aca8b63f5ae200af03eb95fb4b5055a8f990079b5bdf042f568469dd",
|
||||
"sha256:b14d948e6dce389f9a7afc666d60dd1e35fa2138a8ec5306d30cd2e30d36b40c",
|
||||
"sha256:b47839b53956e2737229d70714f1d75f33e8ac26e52c267f0197b3189ca6de24",
|
||||
"sha256:b6d9ec061b9eca86e4dcc003d93334b95d53909afd5a32c6e4f222157b50c071",
|
||||
"sha256:b891880c187e96339474af2a3b2bfb11a8e4732ff5034be919aa9029484cd201",
|
||||
"sha256:bca8fccc15e3add173da91be8f34121578dc777711ffd98d399be35487c934bf",
|
||||
"sha256:c81703b12475da73a5d66618856d04b1307e43428a7e59d98cfe5a5d608a74c6",
|
||||
"sha256:d2507ee9c99dbddd191c86f0e0c8b724c76d26b0602db9ea23232304382e1f21",
|
||||
"sha256:e36cd7b9d4d920d3bfc2369840da506fa68258f7bb176b8743189793c055e43d",
|
||||
"sha256:e7d84b479ddf39fe3ea05387f10b779155fc0990125f4fb35d636114e1c63a2e",
|
||||
"sha256:eac9af361e0d98335a02ff12fb56caeb7ea1196cf1a49dbf6f17828a131da807",
|
||||
"sha256:edfd858985c23523f4e5a7526ca6ee65ff930207a7ec8a8f57a01eae506aaee7",
|
||||
"sha256:ee9ff50557a942d187ec85462bb0960207e7ec5b19b3b48949263993771c6205",
|
||||
"sha256:f0e822cd7644995d9ba248cb4b67859701748a93e2ab7fc9bc18c599a52e4604",
|
||||
"sha256:f180904f33bdd1e92967923a43c22899e303906d19b2cf8bb547db6653ea6e7d",
|
||||
"sha256:f1d18718f9d78182c6b60f568c9a9cec8a7204d7cb6fad4e511a2ef279e4cb05",
|
||||
"sha256:f4c7bf687303ca47d69f9f0133274958fd672efaa33fb5bcde467862d6c621f0",
|
||||
"sha256:f76176492ff082657ada0d0f10c794b6da5800249ef1692b35cf49b1e93e8ef7"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==4.3.2"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==4.3.3"
|
||||
},
|
||||
"pycryptodomex": {
|
||||
"hashes": [
|
||||
"sha256:09c9401dc06fb3d94cb1ec23b4ea067a25d1f4c6b7b118ff5631d0b5daaab3cc",
|
||||
"sha256:0b2f1982c5bc311f0aab8c293524b861b485d76f7c9ab2c3ac9a25b6f7655975",
|
||||
"sha256:136b284e9246b4ccf4f752d435c80f2c44fc2321c198505de1d43a95a3453b3c",
|
||||
"sha256:1789d89f61f70a4cd5483d4dfa8df7032efab1118f8b9894faae03c967707865",
|
||||
"sha256:2126bc54beccbede6eade00e647106b4f4c21e5201d2b0a73e9e816a01c50905",
|
||||
"sha256:258c4233a3fe5a6341780306a36c6fb072ef38ce676a6d41eec3e591347919e8",
|
||||
"sha256:263de9a96d2fcbc9f5bd3a279f14ea0d5f072adb68ebd324987576ec25da084d",
|
||||
"sha256:50cb18d4dd87571006fd2447ccec85e6cec0136632a550aa29226ba075c80644",
|
||||
"sha256:5b883e1439ab63af976656446fb4839d566bb096f15fc3c06b5a99cde4927188",
|
||||
"sha256:5d73e9fa3fe830e7b6b42afc49d8329b07a049a47d12e0ef9225f2fd220f19b2",
|
||||
"sha256:61056a1fd3254f6f863de94c233b30dd33bc02f8c935b2000269705f1eeeffa4",
|
||||
"sha256:67c8eb79ab33d0fbcb56842992298ddb56eb6505a72369c20f60bc1d2b6fb002",
|
||||
"sha256:6e45bb4635b3c4e0a00ca9df75ef6295838c85c2ac44ad882410cb631ed1eeaa",
|
||||
"sha256:7cb51096a6a8d400724104db8a7e4f2206041a1f23e58924aa3d8d96bcb48338",
|
||||
"sha256:800a2b05cfb83654df80266692f7092eeefe2a314fa7901dcefab255934faeec",
|
||||
"sha256:8df69e41f7e7015a90b94d1096ec3d8e0182e73449487306709ec27379fff761",
|
||||
"sha256:917033016ecc23c8933205585a0ab73e20020fdf671b7cd1be788a5c4039840b",
|
||||
"sha256:a12144d785518f6491ad334c75ccdc6ad52ea49230b4237f319dbb7cef26f464",
|
||||
"sha256:a3866d68e2fc345162b1b9b83ef80686acfe5cec0d134337f3b03950a0a8bf56",
|
||||
"sha256:a588a1cb7781da9d5e1c84affd98c32aff9c89771eac8eaa659d2760666f7139",
|
||||
"sha256:a77b79852175064c822b047fee7cf5a1f434f06ad075cc9986aa1c19a0c53eb0",
|
||||
"sha256:af83a554b3f077564229865c45af0791be008ac6469ef0098152139e6bd4b5b6",
|
||||
"sha256:b801216c48c0886742abf286a9a6b117e248ca144d8ceec1f931ce2dd0c9cb40",
|
||||
"sha256:bfb040b5dda1dff1e197d2ef71927bd6b8bfcb9793bc4dfe0bb6df1e691eaacb",
|
||||
"sha256:c01678aee8ac0c1a461cbc38ad496f953f9efcb1fa19f5637cbeba7544792a53",
|
||||
"sha256:c74eb1f73f788facece7979ce91594dc177e1a9b5d5e3e64697dd58299e5cb4d",
|
||||
"sha256:c9a68a2f7bd091ccea54ad3be3e9d65eded813e6d79fdf4cc3604e26cdd6384f",
|
||||
"sha256:d4dd3b381ff5a5907a3eb98f5f6d32c64d319a840278ceea1dcfcc65063856f3",
|
||||
"sha256:e8e5ecbd4da4157889fce8ba49da74764dd86c891410bfd6b24969fa46edda51",
|
||||
"sha256:eb2fc0ec241bf5e5ef56c8fbec4a2634d631e4c4f616a59b567947a0f35ad83c",
|
||||
"sha256:edbe083c299835de7e02c8aa0885cb904a75087d35e7bab75ebe5ed336e8c3e2",
|
||||
"sha256:ff64fd720def623bf64d8776f8d0deada1cc1bf1ec3c1f9d6f5bb5bd098d034f"
|
||||
"sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1",
|
||||
"sha256:108e5f1c1cd70ffce0b68739c75734437c919d2eaec8e85bffc2c8b4d2794305",
|
||||
"sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c",
|
||||
"sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458",
|
||||
"sha256:25cd61e846aaab76d5791d006497134602a9e451e954833018161befc3b5b9ed",
|
||||
"sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc",
|
||||
"sha256:59af01efb011b0e8b686ba7758d59cf4a8263f9ad35911bfe3f416cee4f5c08c",
|
||||
"sha256:5dcac11031a71348faaed1f403a0debd56bf5404232284cf8c761ff918886ebc",
|
||||
"sha256:62a5ec91388984909bb5398ea49ee61b68ecb579123694bffa172c3b0a107079",
|
||||
"sha256:645bd4ca6f543685d643dadf6a856cc382b654cc923460e3a10a49c1b3832aeb",
|
||||
"sha256:653b29b0819605fe0898829c8ad6400a6ccde096146730c2da54eede9b7b8baa",
|
||||
"sha256:69138068268127cd605e03438312d8f271135a33140e2742b417d027a0539427",
|
||||
"sha256:6e186342cfcc3aafaad565cbd496060e5a614b441cacc3995ef0091115c1f6c5",
|
||||
"sha256:76bd15bb65c14900d98835fcd10f59e5e0435077431d3a394b60b15864fddd64",
|
||||
"sha256:7805830e0c56d88f4d491fa5ac640dfc894c5ec570d1ece6ed1546e9df2e98d6",
|
||||
"sha256:7a710b79baddd65b806402e14766c721aee8fb83381769c27920f26476276c1e",
|
||||
"sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43",
|
||||
"sha256:82ee7696ed8eb9a82c7037f32ba9b7c59e51dda6f105b39f043b6ef293989cb3",
|
||||
"sha256:88afd7a3af7ddddd42c2deda43d53d3dfc016c11327d0915f90ca34ebda91499",
|
||||
"sha256:8af1a451ff9e123d0d8bd5d5e60f8e3315c3a64f3cdd6bc853e26090e195cdc8",
|
||||
"sha256:8ee606964553c1a0bc74057dd8782a37d1c2bc0f01b83193b6f8bb14523b877b",
|
||||
"sha256:91852d4480a4537d169c29a9d104dda44094c78f1f5b67bca76c29a91042b623",
|
||||
"sha256:9c682436c359b5ada67e882fec34689726a09c461efd75b6ea77b2403d5665b7",
|
||||
"sha256:bc3ee1b4d97081260d92ae813a83de4d2653206967c4a0a017580f8b9548ddbc",
|
||||
"sha256:bca649483d5ed251d06daf25957f802e44e6bb6df2e8f218ae71968ff8f8edc4",
|
||||
"sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e",
|
||||
"sha256:cbe71b6712429650e3883dc81286edb94c328ffcd24849accac0a4dbcc76958a",
|
||||
"sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781",
|
||||
"sha256:d3584623e68a5064a04748fb6d76117a21a7cb5eaba20608a41c7d0c61721794",
|
||||
"sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea",
|
||||
"sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b",
|
||||
"sha256:ff5c9a67f8a4fba4aed887216e32cbc48f2a6fb2673bb10a99e43be463e15913"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==3.19.0"
|
||||
"version": "==3.20.0"
|
||||
},
|
||||
"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:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84",
|
||||
"sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"
|
||||
"sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472",
|
||||
"sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.0.7"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.2.2"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"alabaster": {
|
||||
"hashes": [
|
||||
"sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3",
|
||||
"sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"
|
||||
"sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e",
|
||||
"sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.7.13"
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"altgraph": {
|
||||
"hashes": [
|
||||
@@ -249,35 +258,27 @@
|
||||
},
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:7d5895c9825e18079c5aeac0572bc2e4c83205c95d416e0b4fee8bc361d2d9ca",
|
||||
"sha256:86b0bb7d7da0be1a7c4aedb7974e391b32d4ed89e33de6ed6902b4b15c97577e"
|
||||
"sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a",
|
||||
"sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25"
|
||||
],
|
||||
"markers": "python_full_version >= '3.8.0'",
|
||||
"version": "==3.0.1"
|
||||
"version": "==3.2.4"
|
||||
},
|
||||
"babel": {
|
||||
"hashes": [
|
||||
"sha256:33e0952d7dd6374af8dbf6768cc4ddf3ccfefc244f9986d4074704f2fbd18900",
|
||||
"sha256:7077a4984b02b6727ac10f1f7294484f737443d7e2e66c5e4380e41a3ae0b4ed"
|
||||
"sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b",
|
||||
"sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.13.1"
|
||||
},
|
||||
"browser-cookie3": {
|
||||
"hashes": [
|
||||
"sha256:114e40fe80853211d65f14bc690a7d90c4c287251593dc5767692a950a9006d4",
|
||||
"sha256:3031ad14b96b47ef1e4c8545f2f463e10ad844ef834dcd0ebdae361e31c6119a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.19.1"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.16.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
|
||||
"sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
|
||||
"sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b",
|
||||
"sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2023.7.22"
|
||||
"version": "==2024.7.4"
|
||||
},
|
||||
"charset-normalizer": {
|
||||
"hashes": [
|
||||
@@ -375,38 +376,30 @@
|
||||
"markers": "python_full_version >= '3.7.0'",
|
||||
"version": "==3.3.2"
|
||||
},
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
|
||||
"sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
|
||||
],
|
||||
"markers": "sys_platform == 'win32'",
|
||||
"version": "==0.4.6"
|
||||
},
|
||||
"dill": {
|
||||
"hashes": [
|
||||
"sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e",
|
||||
"sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"
|
||||
"sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca",
|
||||
"sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.3.7"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.3.8"
|
||||
},
|
||||
"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:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
|
||||
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
|
||||
"sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc",
|
||||
"sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.4"
|
||||
"version": "==3.7"
|
||||
},
|
||||
"imagesize": {
|
||||
"hashes": [
|
||||
@@ -418,126 +411,85 @@
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
"sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504",
|
||||
"sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"
|
||||
"sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109",
|
||||
"sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"
|
||||
],
|
||||
"markers": "python_full_version >= '3.8.0'",
|
||||
"version": "==5.12.0"
|
||||
"version": "==5.13.2"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
|
||||
"sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
|
||||
"sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369",
|
||||
"sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.1.2"
|
||||
},
|
||||
"lz4": {
|
||||
"hashes": [
|
||||
"sha256:0ca83a623c449295bafad745dcd399cea4c55b16b13ed8cfea30963b004016c9",
|
||||
"sha256:0f5614d8229b33d4a97cb527db2a1ac81308c6e796e7bdb5d1309127289f69d5",
|
||||
"sha256:1c4c100d99eed7c08d4e8852dd11e7d1ec47a3340f49e3a96f8dfbba17ffb300",
|
||||
"sha256:1f25eb322eeb24068bb7647cae2b0732b71e5c639e4e4026db57618dcd8279f0",
|
||||
"sha256:200d05777d61ba1ff8d29cb51c534a162ea0b4fe6d3c28be3571a0a48ff36080",
|
||||
"sha256:31d72731c4ac6ebdce57cd9a5cabe0aecba229c4f31ba3e2c64ae52eee3fdb1c",
|
||||
"sha256:3a85b430138882f82f354135b98c320dafb96fc8fe4656573d95ab05de9eb092",
|
||||
"sha256:4931ab28a0d1c133104613e74eec1b8bb1f52403faabe4f47f93008785c0b929",
|
||||
"sha256:4caedeb19e3ede6c7a178968b800f910db6503cb4cb1e9cc9221157572139b49",
|
||||
"sha256:65d5c93f8badacfa0456b660285e394e65023ef8071142e0dcbd4762166e1be0",
|
||||
"sha256:6b50f096a6a25f3b2edca05aa626ce39979d63c3b160687c8c6d50ac3943d0ba",
|
||||
"sha256:7211dc8f636ca625abc3d4fb9ab74e5444b92df4f8d58ec83c8868a2b0ff643d",
|
||||
"sha256:7a9eec24ec7d8c99aab54de91b4a5a149559ed5b3097cf30249b665689b3d402",
|
||||
"sha256:7c2df117def1589fba1327dceee51c5c2176a2b5a7040b45e84185ce0c08b6a3",
|
||||
"sha256:7e2dc1bd88b60fa09b9b37f08553f45dc2b770c52a5996ea52b2b40f25445676",
|
||||
"sha256:83903fe6db92db0be101acedc677aa41a490b561567fe1b3fe68695b2110326c",
|
||||
"sha256:83acfacab3a1a7ab9694333bcb7950fbeb0be21660d236fd09c8337a50817897",
|
||||
"sha256:86480f14a188c37cb1416cdabacfb4e42f7a5eab20a737dac9c4b1c227f3b822",
|
||||
"sha256:867664d9ca9bdfce840ac96d46cd8838c9ae891e859eb98ce82fcdf0e103a947",
|
||||
"sha256:8df16c9a2377bdc01e01e6de5a6e4bbc66ddf007a6b045688e285d7d9d61d1c9",
|
||||
"sha256:8f00a9ba98f6364cadda366ae6469b7b3568c0cced27e16a47ddf6b774169270",
|
||||
"sha256:926b26db87ec8822cf1870efc3d04d06062730ec3279bbbd33ba47a6c0a5c673",
|
||||
"sha256:a6a46889325fd60b8a6b62ffc61588ec500a1883db32cddee9903edfba0b7584",
|
||||
"sha256:a98b61e504fb69f99117b188e60b71e3c94469295571492a6468c1acd63c37ba",
|
||||
"sha256:ad38dc6a7eea6f6b8b642aaa0683253288b0460b70cab3216838747163fb774d",
|
||||
"sha256:b10b77dc2e6b1daa2f11e241141ab8285c42b4ed13a8642495620416279cc5b2",
|
||||
"sha256:d5ea0e788dc7e2311989b78cae7accf75a580827b4d96bbaf06c7e5a03989bd5",
|
||||
"sha256:e05afefc4529e97c08e65ef92432e5f5225c0bb21ad89dee1e06a882f91d7f5e",
|
||||
"sha256:e1431d84a9cfb23e6773e72078ce8e65cad6745816d4cbf9ae67da5ea419acda",
|
||||
"sha256:ec6755cacf83f0c5588d28abb40a1ac1643f2ff2115481089264c7630236618a",
|
||||
"sha256:edc2fb3463d5d9338ccf13eb512aab61937be50aa70734bcf873f2f493801d3b",
|
||||
"sha256:edd8987d8415b5dad25e797043936d91535017237f72fa456601be1479386c92",
|
||||
"sha256:edda4fb109439b7f3f58ed6bede59694bc631c4b69c041112b1b7dc727fffb23",
|
||||
"sha256:f571eab7fec554d3b1db0d666bdc2ad85c81f4b8cb08906c4c59a8cad75e6e22",
|
||||
"sha256:f7c50542b4ddceb74ab4f8b3435327a0861f06257ca501d59067a6a482535a77"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==4.3.2"
|
||||
"version": "==3.1.4"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e",
|
||||
"sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e",
|
||||
"sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431",
|
||||
"sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686",
|
||||
"sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c",
|
||||
"sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559",
|
||||
"sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc",
|
||||
"sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb",
|
||||
"sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939",
|
||||
"sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c",
|
||||
"sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0",
|
||||
"sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4",
|
||||
"sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9",
|
||||
"sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575",
|
||||
"sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba",
|
||||
"sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d",
|
||||
"sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd",
|
||||
"sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3",
|
||||
"sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00",
|
||||
"sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155",
|
||||
"sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac",
|
||||
"sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52",
|
||||
"sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f",
|
||||
"sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8",
|
||||
"sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b",
|
||||
"sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007",
|
||||
"sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24",
|
||||
"sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea",
|
||||
"sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198",
|
||||
"sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0",
|
||||
"sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee",
|
||||
"sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be",
|
||||
"sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2",
|
||||
"sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1",
|
||||
"sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707",
|
||||
"sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6",
|
||||
"sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c",
|
||||
"sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58",
|
||||
"sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823",
|
||||
"sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779",
|
||||
"sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636",
|
||||
"sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c",
|
||||
"sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad",
|
||||
"sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee",
|
||||
"sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc",
|
||||
"sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2",
|
||||
"sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48",
|
||||
"sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7",
|
||||
"sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e",
|
||||
"sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b",
|
||||
"sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa",
|
||||
"sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5",
|
||||
"sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e",
|
||||
"sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb",
|
||||
"sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9",
|
||||
"sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57",
|
||||
"sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc",
|
||||
"sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc",
|
||||
"sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2",
|
||||
"sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"
|
||||
"sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf",
|
||||
"sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff",
|
||||
"sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f",
|
||||
"sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3",
|
||||
"sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532",
|
||||
"sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f",
|
||||
"sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617",
|
||||
"sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df",
|
||||
"sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4",
|
||||
"sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906",
|
||||
"sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f",
|
||||
"sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4",
|
||||
"sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8",
|
||||
"sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371",
|
||||
"sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2",
|
||||
"sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465",
|
||||
"sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52",
|
||||
"sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6",
|
||||
"sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169",
|
||||
"sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad",
|
||||
"sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2",
|
||||
"sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0",
|
||||
"sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029",
|
||||
"sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f",
|
||||
"sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a",
|
||||
"sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced",
|
||||
"sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5",
|
||||
"sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c",
|
||||
"sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf",
|
||||
"sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9",
|
||||
"sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb",
|
||||
"sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad",
|
||||
"sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3",
|
||||
"sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1",
|
||||
"sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46",
|
||||
"sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc",
|
||||
"sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a",
|
||||
"sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee",
|
||||
"sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900",
|
||||
"sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5",
|
||||
"sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea",
|
||||
"sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f",
|
||||
"sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5",
|
||||
"sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e",
|
||||
"sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a",
|
||||
"sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f",
|
||||
"sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50",
|
||||
"sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a",
|
||||
"sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b",
|
||||
"sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4",
|
||||
"sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff",
|
||||
"sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2",
|
||||
"sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46",
|
||||
"sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b",
|
||||
"sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf",
|
||||
"sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5",
|
||||
"sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5",
|
||||
"sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab",
|
||||
"sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd",
|
||||
"sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.1.3"
|
||||
"version": "==2.1.5"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
@@ -549,37 +501,37 @@
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7",
|
||||
"sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e",
|
||||
"sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c",
|
||||
"sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169",
|
||||
"sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208",
|
||||
"sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0",
|
||||
"sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1",
|
||||
"sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1",
|
||||
"sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7",
|
||||
"sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45",
|
||||
"sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143",
|
||||
"sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5",
|
||||
"sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f",
|
||||
"sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd",
|
||||
"sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245",
|
||||
"sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f",
|
||||
"sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332",
|
||||
"sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30",
|
||||
"sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183",
|
||||
"sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f",
|
||||
"sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85",
|
||||
"sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46",
|
||||
"sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71",
|
||||
"sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660",
|
||||
"sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb",
|
||||
"sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c",
|
||||
"sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a"
|
||||
"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.6.1"
|
||||
"version": "==1.11.1"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
@@ -591,11 +543,11 @@
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5",
|
||||
"sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"
|
||||
"sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002",
|
||||
"sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==23.2"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==24.1"
|
||||
},
|
||||
"pefile": {
|
||||
"hashes": [
|
||||
@@ -608,116 +560,79 @@
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3",
|
||||
"sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"
|
||||
"sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee",
|
||||
"sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.11.0"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==4.2.2"
|
||||
},
|
||||
"psutil": {
|
||||
"hashes": [
|
||||
"sha256:10e8c17b4f898d64b121149afb136c53ea8b68c7531155147867b7b1ac9e7e28",
|
||||
"sha256:18cd22c5db486f33998f37e2bb054cc62fd06646995285e02a51b1e08da97017",
|
||||
"sha256:3ebf2158c16cc69db777e3c7decb3c0f43a7af94a60d72e87b2823aebac3d602",
|
||||
"sha256:51dc3d54607c73148f63732c727856f5febec1c7c336f8f41fcbd6315cce76ac",
|
||||
"sha256:6e5fb8dc711a514da83098bc5234264e551ad980cec5f85dabf4d38ed6f15e9a",
|
||||
"sha256:70cb3beb98bc3fd5ac9ac617a327af7e7f826373ee64c80efd4eb2856e5051e9",
|
||||
"sha256:748c9dd2583ed86347ed65d0035f45fa8c851e8d90354c122ab72319b5f366f4",
|
||||
"sha256:91ecd2d9c00db9817a4b4192107cf6954addb5d9d67a969a4f436dbc9200f88c",
|
||||
"sha256:92e0cc43c524834af53e9d3369245e6cc3b130e78e26100d1f63cdb0abeb3d3c",
|
||||
"sha256:a6f01f03bf1843280f4ad16f4bde26b817847b4c1a0db59bf6419807bc5ce05c",
|
||||
"sha256:c69596f9fc2f8acd574a12d5f8b7b1ba3765a641ea5d60fb4736bf3c08a8214a",
|
||||
"sha256:ca2780f5e038379e520281e4c032dddd086906ddff9ef0d1b9dcf00710e5071c",
|
||||
"sha256:daecbcbd29b289aac14ece28eca6a3e60aa361754cf6da3dfb20d4d32b6c7f57",
|
||||
"sha256:e4b92ddcd7dd4cdd3f900180ea1e104932c7bce234fb88976e2a3b296441225a",
|
||||
"sha256:fb8a697f11b0f5994550555fcfe3e69799e5b060c8ecf9e2f75c69302cc35c0d",
|
||||
"sha256:ff18b8d1a784b810df0b0fff3bcb50ab941c3b8e2c8de5726f9c71c601c611aa"
|
||||
"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.6"
|
||||
},
|
||||
"pycryptodomex": {
|
||||
"hashes": [
|
||||
"sha256:09c9401dc06fb3d94cb1ec23b4ea067a25d1f4c6b7b118ff5631d0b5daaab3cc",
|
||||
"sha256:0b2f1982c5bc311f0aab8c293524b861b485d76f7c9ab2c3ac9a25b6f7655975",
|
||||
"sha256:136b284e9246b4ccf4f752d435c80f2c44fc2321c198505de1d43a95a3453b3c",
|
||||
"sha256:1789d89f61f70a4cd5483d4dfa8df7032efab1118f8b9894faae03c967707865",
|
||||
"sha256:2126bc54beccbede6eade00e647106b4f4c21e5201d2b0a73e9e816a01c50905",
|
||||
"sha256:258c4233a3fe5a6341780306a36c6fb072ef38ce676a6d41eec3e591347919e8",
|
||||
"sha256:263de9a96d2fcbc9f5bd3a279f14ea0d5f072adb68ebd324987576ec25da084d",
|
||||
"sha256:50cb18d4dd87571006fd2447ccec85e6cec0136632a550aa29226ba075c80644",
|
||||
"sha256:5b883e1439ab63af976656446fb4839d566bb096f15fc3c06b5a99cde4927188",
|
||||
"sha256:5d73e9fa3fe830e7b6b42afc49d8329b07a049a47d12e0ef9225f2fd220f19b2",
|
||||
"sha256:61056a1fd3254f6f863de94c233b30dd33bc02f8c935b2000269705f1eeeffa4",
|
||||
"sha256:67c8eb79ab33d0fbcb56842992298ddb56eb6505a72369c20f60bc1d2b6fb002",
|
||||
"sha256:6e45bb4635b3c4e0a00ca9df75ef6295838c85c2ac44ad882410cb631ed1eeaa",
|
||||
"sha256:7cb51096a6a8d400724104db8a7e4f2206041a1f23e58924aa3d8d96bcb48338",
|
||||
"sha256:800a2b05cfb83654df80266692f7092eeefe2a314fa7901dcefab255934faeec",
|
||||
"sha256:8df69e41f7e7015a90b94d1096ec3d8e0182e73449487306709ec27379fff761",
|
||||
"sha256:917033016ecc23c8933205585a0ab73e20020fdf671b7cd1be788a5c4039840b",
|
||||
"sha256:a12144d785518f6491ad334c75ccdc6ad52ea49230b4237f319dbb7cef26f464",
|
||||
"sha256:a3866d68e2fc345162b1b9b83ef80686acfe5cec0d134337f3b03950a0a8bf56",
|
||||
"sha256:a588a1cb7781da9d5e1c84affd98c32aff9c89771eac8eaa659d2760666f7139",
|
||||
"sha256:a77b79852175064c822b047fee7cf5a1f434f06ad075cc9986aa1c19a0c53eb0",
|
||||
"sha256:af83a554b3f077564229865c45af0791be008ac6469ef0098152139e6bd4b5b6",
|
||||
"sha256:b801216c48c0886742abf286a9a6b117e248ca144d8ceec1f931ce2dd0c9cb40",
|
||||
"sha256:bfb040b5dda1dff1e197d2ef71927bd6b8bfcb9793bc4dfe0bb6df1e691eaacb",
|
||||
"sha256:c01678aee8ac0c1a461cbc38ad496f953f9efcb1fa19f5637cbeba7544792a53",
|
||||
"sha256:c74eb1f73f788facece7979ce91594dc177e1a9b5d5e3e64697dd58299e5cb4d",
|
||||
"sha256:c9a68a2f7bd091ccea54ad3be3e9d65eded813e6d79fdf4cc3604e26cdd6384f",
|
||||
"sha256:d4dd3b381ff5a5907a3eb98f5f6d32c64d319a840278ceea1dcfcc65063856f3",
|
||||
"sha256:e8e5ecbd4da4157889fce8ba49da74764dd86c891410bfd6b24969fa46edda51",
|
||||
"sha256:eb2fc0ec241bf5e5ef56c8fbec4a2634d631e4c4f616a59b567947a0f35ad83c",
|
||||
"sha256:edbe083c299835de7e02c8aa0885cb904a75087d35e7bab75ebe5ed336e8c3e2",
|
||||
"sha256:ff64fd720def623bf64d8776f8d0deada1cc1bf1ec3c1f9d6f5bb5bd098d034f"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==3.19.0"
|
||||
"version": "==6.0.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692",
|
||||
"sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"
|
||||
"sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199",
|
||||
"sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.16.1"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.18.0"
|
||||
},
|
||||
"pyinstaller": {
|
||||
"hashes": [
|
||||
"sha256:041ab9311d08162356829bf47293a613c44dc9ace28846fb63098889c7383c5d",
|
||||
"sha256:331f050e8f9e923bb6b50454acfc0547fd52092585c61eb5f2fc93de60703f13",
|
||||
"sha256:3c04963637481a3edf1eec64ab4c3fce098908f02fc472c11e73be7eedc08b95",
|
||||
"sha256:4368e4eb9999ce32e3280330b3c26f175e0fa7fa13efb4d2dc4ade488ff6d7c2",
|
||||
"sha256:6e71d9f6f5a1e0f7523e8ebee1b76bb29538f64d863e3711c2b21033f499e2b9",
|
||||
"sha256:8f3d49c60f3344bf3d4a6d4258bda665dad185ab2b097341d3af2a6387c838ef",
|
||||
"sha256:9e8b5bbc1bdf554ade1360e62e4959091430c3cc15ebfff3c28c8894fd1f312a",
|
||||
"sha256:bebf6f442bbe6343acaec873803510ee1930d026846a018f727da4e0690081f8",
|
||||
"sha256:da78942d31c1911ea4abcd3ca3bd0c062af7f163a5e227fd18a359b61deda4ca",
|
||||
"sha256:dd438afd2abb643f5399c0cb254a11c217c06782cb274a2911dd785f9f67fa9e",
|
||||
"sha256:f63d2353537bac7bfeeaedbe5ac99f3be35daa290dd1ad1be90768acbf77e3d5",
|
||||
"sha256:f9f5bcaef6122d93c54ee7a9ecb07eab5b81a7ebfb5cb99af2b2a6ff49eff62f"
|
||||
"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.1.0"
|
||||
"version": "==6.9.0"
|
||||
},
|
||||
"pyinstaller-hooks-contrib": {
|
||||
"hashes": [
|
||||
"sha256:4b4a998036abb713774cb26534ca06b7e6e09e4c628196017a10deb11a48747f",
|
||||
"sha256:6dc1786a8f452941245d5bb85893e2a33632ebdcbc4c23eea41f2ee08281b0c0"
|
||||
"sha256:0057fe9a5c398d3f580e73e58793a1d4a8315ca91c3df01efea1c14ed557825a",
|
||||
"sha256:29b68d878ab739e967055b56a93eb9b58e529d5b054fbab7a2f2bacf80cef3e2"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2023.10"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2024.8"
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:0d4c286ef6d2f66c8bfb527a7f8a629009e42c99707dec821a03e1b51a4c1496",
|
||||
"sha256:60ed5f3a9ff8b61839ff0348b3624ceeb9e6c2a92c514d81c9cc273da3b6bcda"
|
||||
"sha256:03c8e3baa1d9fb995b12c1dbe00aa6c4bcef210c2a2634374aedeb22fb4a8f8f",
|
||||
"sha256:a5d01678349454806cff6d886fb072294f56a58c4761278c97fb557d708e1eb3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_full_version >= '3.8.0'",
|
||||
"version": "==3.0.2"
|
||||
"version": "==3.2.6"
|
||||
},
|
||||
"pywin32-ctypes": {
|
||||
"hashes": [
|
||||
@@ -730,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:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87",
|
||||
"sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"
|
||||
"sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1",
|
||||
"sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==68.2.2"
|
||||
"version": "==72.1.0"
|
||||
},
|
||||
"snowballstemmer": {
|
||||
"hashes": [
|
||||
@@ -754,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:06683a2b76c3c7b1931b75e40e0211866fbb50ba4c4e802d0901d9b4e849add2",
|
||||
"sha256:4cc16c5545f2bf896ca52a854babefe3d8baeaaa033d13a7f179ac1d9feb02d5"
|
||||
"sha256:b7058e8c5831e5598afca1a78fda0695d3291388d954464a6e480c36198680c0",
|
||||
"sha256:fde3d888949bd0a91207cf1e54afda58121dbb4bf1f183d0cc78a0826654c974"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.24.1"
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==2.2.3"
|
||||
},
|
||||
"sphinxcontrib-applehelp": {
|
||||
"hashes": [
|
||||
"sha256:094c4d56209d1734e7d252f6e0b3ccc090bd52ee56807a5d9315b19c122ab15d",
|
||||
"sha256:39fdc8d762d33b01a7d8f026a3b7d71563ea3b72787d5f00ad8465bd9d6dfbfa"
|
||||
"sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1",
|
||||
"sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==1.0.7"
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"sphinxcontrib-devhelp": {
|
||||
"hashes": [
|
||||
"sha256:63b41e0d38207ca40ebbeabcf4d8e51f76c03e78cd61abe118cf4435c73d4212",
|
||||
"sha256:fe8009aed765188f08fcaadbb3ea0d90ce8ae2d76710b7e29ea7d047177dae2f"
|
||||
"sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad",
|
||||
"sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==1.0.5"
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"sphinxcontrib-htmlhelp": {
|
||||
"hashes": [
|
||||
"sha256:6c26a118a05b76000738429b724a0568dbde5b72391a688577da08f11891092a",
|
||||
"sha256:8001661c077a73c29beaf4a79968d0726103c5605e27db92b9ebed8bab1359e9"
|
||||
"sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8",
|
||||
"sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==2.0.4"
|
||||
"version": "==2.1.0"
|
||||
},
|
||||
"sphinxcontrib-jquery": {
|
||||
"hashes": [
|
||||
@@ -813,19 +728,19 @@
|
||||
},
|
||||
"sphinxcontrib-qthelp": {
|
||||
"hashes": [
|
||||
"sha256:62b9d1a186ab7f5ee3356d906f648cacb7a6bdb94d201ee7adf26db55092982d",
|
||||
"sha256:bf76886ee7470b934e363da7a954ea2825650013d367728588732c7350f49ea4"
|
||||
"sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab",
|
||||
"sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==1.0.6"
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"sphinxcontrib-serializinghtml": {
|
||||
"hashes": [
|
||||
"sha256:0c64ff898339e1fac29abd2bf5f11078f3ec413cfe9c046d3120d7ca65530b54",
|
||||
"sha256:9b36e503703ff04f20e9675771df105e58aa029cfcbc23b8ed716019b7416ae1"
|
||||
"sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331",
|
||||
"sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==1.1.9"
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"tomli": {
|
||||
"hashes": [
|
||||
@@ -838,36 +753,36 @@
|
||||
},
|
||||
"tomlkit": {
|
||||
"hashes": [
|
||||
"sha256:df32fab589a81f0d7dc525a4267b6d7a64ee99619cbd1eeb0fae32c1dd426977",
|
||||
"sha256:eeea7ac7563faeab0a1ed8fe12c2e5a51c61f933f2502f7e9db0241a65163ad0"
|
||||
"sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72",
|
||||
"sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.12.2"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==0.13.0"
|
||||
},
|
||||
"types-requests": {
|
||||
"hashes": [
|
||||
"sha256:b32b9a86beffa876c0c3ac99a4cd3b8b51e973fb8e3bd4e0a6bb32c7efad80fc",
|
||||
"sha256:dc5852a76f1eaf60eafa81a2e50aefa3d1f015c34cf0cba130930866b1b22a92"
|
||||
"sha256:90c079ff05e549f6bf50e02e910210b98b8ff1ebdd18e19c873cd237737c1358",
|
||||
"sha256:f754283e152c752e46e70942fa2a146b5bc70393522257bb85bd1ef7e019dcc3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.31.0.10"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.32.0.20240712"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0",
|
||||
"sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"
|
||||
"sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d",
|
||||
"sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==4.8.0"
|
||||
"version": "==4.12.2"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84",
|
||||
"sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"
|
||||
"sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472",
|
||||
"sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.0.7"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.2.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
@@ -125,7 +125,6 @@ Supporters
|
||||
|
||||
| Instaloader is proudly sponsored by
|
||||
| `@rocketapi-io <https://github.com/rocketapi-io>`__
|
||||
| `@socialmethod <https://github.com/socialmethod>`__
|
||||
|
||||
See `Alex' GitHub Sponsors <https://github.com/sponsors/aandergr>`__ page for
|
||||
how you can sponsor the development of Instaloader!
|
||||
|
||||
@@ -44,7 +44,7 @@ with open('__main__.py', 'w+') as f:
|
||||
f.writelines(lines)
|
||||
|
||||
# install dependencies and invoke PyInstaller
|
||||
commands = ["pip install pipenv==2023.10.3",
|
||||
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"]
|
||||
|
||||
|
||||
+49
-11
@@ -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,6 +92,9 @@ 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**.
|
||||
|
||||
@@ -102,27 +105,28 @@ Instaloader supports the following targets:
|
||||
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
|
||||
|
||||
+28
-7
@@ -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,18 +199,32 @@ 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.
|
||||
|
||||
.. option:: --load-cookies BROWSER-NAME, -b BROWSER-NAME
|
||||
|
||||
Use Instagram cookie in your browser to login (works with :option:`--login`). Supported browsers: Chrome, Firefox, Edge, Opera, Safari, Brave.
|
||||
Use Instagram cookie in your browser to login.
|
||||
This feature requires the browser_cookie3 library.
|
||||
Compatible with :option:`--cookiefile` if you want to load cookies from browser profiles.
|
||||
Incompatible with :option:`--login` due to potential username mismatch between user input and browser login.
|
||||
Supported browsers: Brave, Chrome, Chromium, Edge, Firefox, LibreWolf, Opera, Opera_GX, Safari and Vivaldi.
|
||||
|
||||
In subsequent runs, you can just use :option:`--login` to reuse the
|
||||
same session, which is saved by Instaloader.
|
||||
|
||||
.. versionadded:: 4.11
|
||||
|
||||
.. option:: --cookiefile COOKIE-FILE, -B COOKIE-FILE
|
||||
|
||||
Cookie file path of a browser profile to load cookies from.
|
||||
|
||||
.. versionadded:: 4.11
|
||||
|
||||
.. option:: --sessionfile SESSIONFILE, -f SESSIONFILE
|
||||
|
||||
Path for loading and storing session key file. Defaults to
|
||||
@@ -289,7 +310,7 @@ How to Download
|
||||
.. option:: --user-agent USER_AGENT
|
||||
|
||||
User Agent to use for HTTP requests. Per default, Instaloader pretends being
|
||||
Chrome/119 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
-8
@@ -1,7 +1,7 @@
|
||||
"""Download pictures (or videos) along with their captions and other metadata from Instagram."""
|
||||
|
||||
|
||||
__version__ = '4.11rc1'
|
||||
__version__ = '4.14'
|
||||
|
||||
|
||||
try:
|
||||
@@ -13,10 +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)
|
||||
|
||||
+135
-70
@@ -6,10 +6,11 @@ import os
|
||||
import re
|
||||
import sys
|
||||
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
|
||||
@@ -21,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), '')
|
||||
@@ -71,20 +81,25 @@ def filterstr_to_filterfunc(filter_str: str, item_type: type):
|
||||
|
||||
return filterfunc
|
||||
|
||||
|
||||
def get_cookies_from_instagram(domain, browser, cookie_file='', cookie_name=''):
|
||||
supported_browsers = {
|
||||
"chrome": browser_cookie3.chrome,
|
||||
"firefox": browser_cookie3.firefox,
|
||||
"edge": browser_cookie3.edge,
|
||||
"brave": browser_cookie3.brave,
|
||||
"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))
|
||||
@@ -96,21 +111,26 @@ 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 succesfully in {browser}?")
|
||||
|
||||
if cookie_name:
|
||||
return cookies.get(cookie_name, {})
|
||||
else:
|
||||
return cookies
|
||||
|
||||
|
||||
def import_session(browser, instaloader, cookiefile):
|
||||
cookie = get_cookies_from_instagram('instagram', browser, cookiefile)
|
||||
if cookie is not None:
|
||||
instaloader.context.update_cookies(cookie)
|
||||
username = instaloader.test_login()
|
||||
if not username:
|
||||
raise SystemExit(f"Not logged in. Are you logged in successfully in {browser}?")
|
||||
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.")
|
||||
print(f"Next time use --login={username} to reuse the same session.")
|
||||
|
||||
|
||||
def _main(instaloader: Instaloader, targetlist: List[str],
|
||||
username: Optional[str] = None, password: Optional[str] = None,
|
||||
@@ -119,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
|
||||
@@ -140,6 +161,11 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
||||
if latest_stamps_file is not None:
|
||||
latest_stamps = LatestStamps(latest_stamps_file)
|
||||
instaloader.context.log(f"Using latest stamps from {latest_stamps_file}.")
|
||||
# load cookies if browser is not None
|
||||
if browser and bc3_library:
|
||||
import_session(browser.lower(), instaloader, cookiefile)
|
||||
elif browser and not bc3_library:
|
||||
raise 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):
|
||||
@@ -150,11 +176,6 @@ def _main(instaloader: Instaloader, targetlist: List[str],
|
||||
if sessionfile is not None:
|
||||
print(err, file=sys.stderr)
|
||||
instaloader.context.log("Session file does not exist yet - Logging in.")
|
||||
if browser is not None:
|
||||
if bc3_library:
|
||||
import_session(browser.lower(), instaloader, cookiefile)
|
||||
else:
|
||||
raise SystemExit("browser_cookie3 library is needed to load cookies from browsers")
|
||||
if not instaloader.context.is_logged_in or username != instaloader.test_login():
|
||||
if password is not None:
|
||||
try:
|
||||
@@ -173,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:
|
||||
@@ -233,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:
|
||||
@@ -247,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:
|
||||
@@ -271,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)
|
||||
@@ -299,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():
|
||||
@@ -314,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]",
|
||||
@@ -355,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',
|
||||
@@ -373,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.')
|
||||
|
||||
@@ -409,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',
|
||||
@@ -476,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
|
||||
@@ -494,15 +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 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
|
||||
@@ -527,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
|
||||
|
||||
|
||||
|
||||
+82
-17
@@ -77,7 +77,7 @@ 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
|
||||
|
||||
@@ -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
|
||||
@@ -991,7 +997,8 @@ class Instaloader:
|
||||
max_count: Optional[int] = None,
|
||||
total_count: Optional[int] = None,
|
||||
owner_profile: Optional[Profile] = None,
|
||||
takewhile: Optional[Callable[[Post], bool]] = None) -> None:
|
||||
takewhile: Optional[Callable[[Post], bool]] = None,
|
||||
possibly_pinned: int = 0) -> None:
|
||||
"""
|
||||
Download the Posts returned by given Post Iterator.
|
||||
|
||||
@@ -1003,6 +1010,9 @@ class Instaloader:
|
||||
.. versionchanged:: 4.8
|
||||
Add `takewhile` parameter.
|
||||
|
||||
.. versionchanged:: 4.10.3
|
||||
Add `possibly_pinned` parameter.
|
||||
|
||||
:param posts: Post Iterator to loop through.
|
||||
:param target: Target name.
|
||||
:param fast_update: :option:`--fast-update`.
|
||||
@@ -1011,6 +1021,8 @@ class Instaloader:
|
||||
:param total_count: Total number of posts returned by given iterator.
|
||||
:param owner_profile: Associated profile, if any.
|
||||
:param takewhile: Expression evaluated for each post. Once it returns false, downloading stops.
|
||||
:param possibly_pinned: Number of posts that might be pinned. These posts do not cause download
|
||||
to stop even if they've already been downloaded.
|
||||
"""
|
||||
displayed_count = (max_count if total_count is None or max_count is not None and max_count < total_count
|
||||
else total_count)
|
||||
@@ -1032,7 +1044,7 @@ class Instaloader:
|
||||
) as (is_resuming, start_index):
|
||||
for number, post in enumerate(posts, start=start_index + 1):
|
||||
should_stop = not takewhile(post)
|
||||
if should_stop and post.is_pinned:
|
||||
if should_stop and number <= possibly_pinned:
|
||||
continue
|
||||
if (max_count is not None and number > max_count) or should_stop:
|
||||
break
|
||||
@@ -1066,7 +1078,7 @@ class Instaloader:
|
||||
except PostChangedException:
|
||||
post_changed = True
|
||||
continue
|
||||
if fast_update and not downloaded and not post_changed and not post.is_pinned:
|
||||
if fast_update and not downloaded and not post_changed and number > possibly_pinned:
|
||||
# disengage fast_update for first post when resuming
|
||||
if not is_resuming or number > 0:
|
||||
break
|
||||
@@ -1269,6 +1281,31 @@ 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,
|
||||
)
|
||||
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:
|
||||
@@ -1398,7 +1435,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.
|
||||
@@ -1415,6 +1454,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
|
||||
|
||||
@@ -1423,6 +1464,12 @@ class Instaloader:
|
||||
|
||||
.. versionchanged:: 4.8
|
||||
Add `latest_stamps` parameter.
|
||||
|
||||
.. versionchanged:: 4.13
|
||||
Add `max_count` parameter.
|
||||
|
||||
.. versionchanged:: 4.14
|
||||
Add `reels` parameter.
|
||||
"""
|
||||
|
||||
@contextmanager
|
||||
@@ -1454,7 +1501,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):
|
||||
@@ -1466,6 +1513,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)):
|
||||
@@ -1488,7 +1541,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)
|
||||
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)
|
||||
@@ -1576,11 +1629,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:
|
||||
@@ -1599,3 +1657,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,16 +1,14 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
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,8 +31,8 @@ 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/119.0.0.0 Safari/537.36'
|
||||
return ('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
|
||||
'(KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36')
|
||||
|
||||
|
||||
def default_iphone_headers() -> Dict[str, Any]:
|
||||
@@ -95,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()
|
||||
@@ -152,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:
|
||||
@@ -212,6 +216,7 @@ class InstaloaderContext:
|
||||
return requests.utils.dict_from_cookiejar(self._session.cookies)
|
||||
|
||||
def update_cookies(self, cookie):
|
||||
""".. versionadded:: 4.11"""
|
||||
self._session.cookies.update(cookie)
|
||||
|
||||
def load_session(self, username, sessiondata):
|
||||
@@ -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
|
||||
@@ -259,20 +273,25 @@ class InstaloaderContext:
|
||||
# Override default timeout behavior.
|
||||
# Need to silence mypy bug for this. See: https://github.com/python/mypy/issues/2427
|
||||
session.request = partial(session.request, timeout=self.request_timeout) # type: ignore
|
||||
csrf_json = self.get_json('accounts/login/', {}, session=session)
|
||||
csrf_token = csrf_json['config']['csrf_token']
|
||||
|
||||
# Make a request to Instagram's root URL, which will set the session's csrftoken cookie
|
||||
# Not using self.get_json() here, because we need to access the cookie
|
||||
session.get('https://www.instagram.com/')
|
||||
# Add session's csrftoken cookie to session headers
|
||||
csrf_token = session.cookies.get_dict()['csrftoken']
|
||||
session.headers.update({'X-CSRFToken': csrf_token})
|
||||
# Not using self.get_json() here, because we need to access csrftoken cookie
|
||||
|
||||
self.do_sleep()
|
||||
# Workaround credits to pgrimaud.
|
||||
# See: https://github.com/pgrimaud/instagram-user-feed/commit/96ad4cf54d1ad331b337f325c73e664999a6d066
|
||||
enc_password = '#PWD_INSTAGRAM_BROWSER:0:{}:{}'.format(int(datetime.now().timestamp()), passwd)
|
||||
login = session.post('https://www.instagram.com/accounts/login/ajax/',
|
||||
login = session.post('https://www.instagram.com/api/v1/web/accounts/login/ajax/',
|
||||
data={'enc_password': enc_password, 'username': user}, allow_redirects=True)
|
||||
try:
|
||||
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'):
|
||||
@@ -284,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
|
||||
@@ -346,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 = ""
|
||||
@@ -387,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)):
|
||||
@@ -399,31 +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))
|
||||
is_html_query = not is_graphql_query and not "__a" in params and host == "www.instagram.com"
|
||||
if is_html_query:
|
||||
# Extract JSON from HTML response
|
||||
match = re.search('(?<={"raw":").*?(?<!\\\\)(?=")', resp.text)
|
||||
if match is None:
|
||||
raise QueryReturnedNotFoundException("Could not find JSON data in html response.")
|
||||
# Unescape escaped JSON string
|
||||
unescaped_string = match.group(0).encode("utf-8").decode("unicode_escape")
|
||||
resp_json = json.loads(unescaped_string)
|
||||
return resp_json
|
||||
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)
|
||||
@@ -437,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:
|
||||
@@ -448,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:
|
||||
@@ -470,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
|
||||
@@ -487,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.
|
||||
@@ -499,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:
|
||||
@@ -612,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.
|
||||
@@ -642,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)
|
||||
|
||||
+47
-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
|
||||
@@ -184,6 +203,10 @@ class NodeIterator(Iterator[T]):
|
||||
"""
|
||||
return self._node_wrapper(self._first_node) if self._first_node is not None else None
|
||||
|
||||
@staticmethod
|
||||
def page_length() -> int:
|
||||
return NodeIterator._graphql_page_length
|
||||
|
||||
def freeze(self) -> FrozenNodeIterator:
|
||||
"""Freeze the iterator for later resuming."""
|
||||
remaining_data = None
|
||||
@@ -199,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:
|
||||
@@ -216,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.")
|
||||
|
||||
+187
-38
@@ -51,6 +51,29 @@ class PostComment:
|
||||
self._answers = answers
|
||||
self._post = post
|
||||
|
||||
@classmethod
|
||||
def from_iphone_struct(
|
||||
cls,
|
||||
context: "InstaloaderContext",
|
||||
media: Dict[str, Any],
|
||||
answers: Iterator["PostCommentAnswer"],
|
||||
post: "Post",
|
||||
):
|
||||
return cls(
|
||||
context=context,
|
||||
node={
|
||||
"id": int(media["pk"]),
|
||||
"created_at": media["created_at"],
|
||||
"text": media["text"],
|
||||
"edge_liked_by": {
|
||||
"count": media["comment_like_count"],
|
||||
},
|
||||
"iphone_struct": media,
|
||||
},
|
||||
answers=answers,
|
||||
post=post,
|
||||
)
|
||||
|
||||
@property
|
||||
def id(self) -> int:
|
||||
""" ID number of comment. """
|
||||
@@ -67,9 +90,13 @@ class PostComment:
|
||||
return self._node['text']
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
def owner(self) -> "Profile":
|
||||
""" Owner :class:`Profile` of the comment. """
|
||||
return Profile(self._context, self._node['owner'])
|
||||
if "iphone_struct" in self._node:
|
||||
return Profile.from_iphone_struct(
|
||||
self._context, self._node["iphone_struct"]["user"]
|
||||
)
|
||||
return Profile(self._context, self._node["owner"])
|
||||
|
||||
@property
|
||||
def likes_count(self):
|
||||
@@ -205,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:
|
||||
@@ -281,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
|
||||
@@ -303,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]
|
||||
@@ -590,16 +638,77 @@ class Post:
|
||||
except KeyError:
|
||||
return self._field('edge_media_to_comment', 'count')
|
||||
|
||||
def _get_comments_via_iphone_endpoint(self) -> Iterable[PostComment]:
|
||||
"""
|
||||
Iterate over all comments of the post via an iPhone endpoint.
|
||||
|
||||
.. versionadded:: 4.10.3
|
||||
fallback for :issue:`2125`.
|
||||
"""
|
||||
def _query(min_id=None):
|
||||
pagination_params = {"min_id": min_id} if min_id is not None else {}
|
||||
return self._context.get_iphone_json(
|
||||
f"api/v1/media/{self.mediaid}/comments/",
|
||||
{
|
||||
"can_support_threading": "true",
|
||||
"permalink_enabled": "false",
|
||||
**pagination_params,
|
||||
},
|
||||
)
|
||||
|
||||
def _answers(comment_node):
|
||||
def _answer(child_comment):
|
||||
return PostCommentAnswer(
|
||||
id=int(child_comment["pk"]),
|
||||
created_at_utc=datetime.utcfromtimestamp(child_comment["created_at"]),
|
||||
text=child_comment["text"],
|
||||
owner=Profile.from_iphone_struct(self._context, child_comment["user"]),
|
||||
likes_count=child_comment["comment_like_count"],
|
||||
)
|
||||
|
||||
child_comment_count = comment_node["child_comment_count"]
|
||||
if child_comment_count == 0:
|
||||
return
|
||||
preview_child_comments = comment_node["preview_child_comments"]
|
||||
if child_comment_count == len(preview_child_comments):
|
||||
yield from (
|
||||
_answer(child_comment) for child_comment in preview_child_comments
|
||||
)
|
||||
return
|
||||
pk = comment_node["pk"]
|
||||
answers_json = self._context.get_iphone_json(
|
||||
f"api/v1/media/{self.mediaid}/comments/{pk}/child_comments/",
|
||||
{"max_id": ""},
|
||||
)
|
||||
yield from (
|
||||
_answer(child_comment) for child_comment in answers_json["child_comments"]
|
||||
)
|
||||
|
||||
def _paginated_comments(comments_json):
|
||||
for comment_node in comments_json.get("comments", []):
|
||||
yield PostComment.from_iphone_struct(
|
||||
self._context, comment_node, _answers(comment_node), self
|
||||
)
|
||||
|
||||
next_min_id = comments_json.get("next_min_id")
|
||||
if next_min_id:
|
||||
yield from _paginated_comments(_query(next_min_id))
|
||||
|
||||
return _paginated_comments(_query())
|
||||
|
||||
def get_comments(self) -> Iterable[PostComment]:
|
||||
r"""Iterate over all comments of the post.
|
||||
"""Iterate over all comments of the post.
|
||||
|
||||
Each comment is represented by a PostComment NamedTuple with fields text (string), created_at (datetime),
|
||||
id (int), owner (:class:`Profile`) and answers (:class:`~typing.Iterator`\ [:class:`PostCommentAnswer`])
|
||||
id (int), owner (:class:`Profile`) and answers (:class:`~typing.Iterator` [:class:`PostCommentAnswer`])
|
||||
if available.
|
||||
|
||||
.. 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']),
|
||||
@@ -635,12 +744,22 @@ 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:
|
||||
# If the Post's metadata already contains all parent comments, don't do GraphQL requests to obtain them
|
||||
return [_postcomment(comment['node']) for comment in comment_edges]
|
||||
|
||||
if self.comments > NodeIterator.page_length():
|
||||
# comments pagination via our graphql query does not work reliably anymore (issue #2125), fallback to an
|
||||
# iphone endpoint if needed.
|
||||
return self._get_comments_via_iphone_endpoint()
|
||||
|
||||
return NodeIterator(
|
||||
self._context,
|
||||
'97b41c52301f77ce508f55e66d17620e',
|
||||
@@ -658,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
|
||||
@@ -723,7 +842,11 @@ class Post:
|
||||
|
||||
@property
|
||||
def is_pinned(self) -> bool:
|
||||
"""True if this Post has been pinned by at least one user.
|
||||
"""
|
||||
.. deprecated: 4.10.3
|
||||
This information is not returned by IG anymore
|
||||
|
||||
Used to return True if this Post has been pinned by at least one user, now likely returns always false.
|
||||
|
||||
.. versionadded: 4.9.2"""
|
||||
return 'pinned_for_users' in self._node and bool(self._node['pinned_for_users'])
|
||||
@@ -799,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:
|
||||
@@ -831,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):
|
||||
@@ -885,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']
|
||||
@@ -1076,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]:
|
||||
@@ -1092,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,
|
||||
@@ -1120,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.
|
||||
|
||||
@@ -1152,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,
|
||||
@@ -1171,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,
|
||||
@@ -1190,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,
|
||||
@@ -1209,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",
|
||||
@@ -1288,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={}
|
||||
|
||||
@@ -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