mirror of
https://github.com/instaloader/instaloader.git
synced 2026-10-16 13:48:03 -07:00
Merge branch 'master' into upcoming/v4.16
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: pip
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: monthly
|
||||
cooldown:
|
||||
default-days: 7
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: monthly
|
||||
cooldown:
|
||||
default-days: 7
|
||||
@@ -8,17 +8,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout Instaloader Repository"
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0 # needed for building docs
|
||||
persist-credentials: false
|
||||
- name: "Setup Python"
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: 3.14
|
||||
python-version: 3.15
|
||||
cache: 'pipenv'
|
||||
- name: "Install Dependencies"
|
||||
run: |
|
||||
python -m pip install pipenv==2025.0.4
|
||||
python -m pip install pipenv==2026.8.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"
|
||||
|
||||
@@ -9,21 +9,22 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
|
||||
steps:
|
||||
- name: Checkout Instaloader Repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pipenv'
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
python -m pip install pipenv==2025.0.4
|
||||
python -m pip install pipenv==2026.8.0
|
||||
pipenv --python `python --version | grep -Eo '3\.[0-9]+'` sync --dev
|
||||
- name: PyLint
|
||||
if: matrix.python-version != '3.9'
|
||||
run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run pylint instaloader
|
||||
- name: MyPy
|
||||
run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run mypy -m instaloader
|
||||
|
||||
@@ -11,11 +11,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: "Checkout Repository"
|
||||
uses: actions/checkout@v4
|
||||
- name: "Setup Python"
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
python-version: 3.14
|
||||
persist-credentials: false
|
||||
- name: "Setup Python"
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: 3.15
|
||||
- name: "Get the tagged version"
|
||||
id: get_version
|
||||
env:
|
||||
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@v11
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
only-labels: 'question'
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
days-before-stale: 21
|
||||
days-before-close: -1
|
||||
remove-stale-when-updated: false
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@v11
|
||||
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.'
|
||||
|
||||
@@ -8,11 +8,13 @@ jobs:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout Instaloader repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
python-version: "3.14"
|
||||
persist-credentials: false
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: 3.15
|
||||
cache: 'pipenv'
|
||||
architecture: x64
|
||||
- name: Get the tagged version
|
||||
@@ -28,7 +30,7 @@ jobs:
|
||||
- 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: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
|
||||
Generated
+1054
-592
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,7 @@ with open('__main__.py', 'w+') as f:
|
||||
f.writelines(lines)
|
||||
|
||||
# install dependencies and invoke PyInstaller
|
||||
commands = ["pip install pipenv==2025.0.4",
|
||||
commands = ["pip install pipenv==2026.8.0",
|
||||
f"pipenv --python {sys.version_info.major}.{sys.version_info.minor} sync --dev",
|
||||
"pipenv run pyinstaller --log-level=DEBUG instaloader.spec"]
|
||||
|
||||
|
||||
+4
-3
@@ -7,9 +7,10 @@ https://instaloader.github.io/
|
||||
|
||||
## Build
|
||||
|
||||
The documentation is created with [Sphinx](http://www.sphinx-doc.org/). To build it, use
|
||||
The documentation is created with [Sphinx](http://www.sphinx-doc.org/). To build it, use (in the top-level directory
|
||||
of the Instaloader source tree):
|
||||
|
||||
```
|
||||
pip3 install -r requirements.txt
|
||||
make html
|
||||
pipenv sync --dev
|
||||
make -C docs html
|
||||
```
|
||||
|
||||
@@ -14,8 +14,13 @@ def get_cookiefile():
|
||||
default_cookiefile = {
|
||||
"Windows": "~/AppData/Roaming/Mozilla/Firefox/Profiles/*/cookies.sqlite",
|
||||
"Darwin": "~/Library/Application Support/Firefox/Profiles/*/cookies.sqlite",
|
||||
}.get(system(), "~/.mozilla/firefox/*/cookies.sqlite")
|
||||
cookiefiles = glob(expanduser(default_cookiefile))
|
||||
}.get(system(), None)
|
||||
if default_cookiefile:
|
||||
cookiefiles = glob(expanduser(default_cookiefile))
|
||||
else:
|
||||
# Check Firefox 147+ XDG path, then legacy
|
||||
cookiefiles = glob(expanduser("~/.config/mozilla/firefox/*/cookies.sqlite")) or \
|
||||
glob(expanduser("~/.mozilla/firefox/*/cookies.sqlite"))
|
||||
if not cookiefiles:
|
||||
raise SystemExit("No Firefox cookies.sqlite file found. Use -c COOKIEFILE.")
|
||||
return cookiefiles[0]
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
requests
|
||||
sphinx~=2.3
|
||||
@@ -330,6 +330,9 @@ class Instaloader:
|
||||
def __exit__(self, *args):
|
||||
self.close()
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Instaloader context={self.context}>"
|
||||
|
||||
@_retry_on_connection_error
|
||||
def download_pic(self, filename: str, url: str, mtime: datetime,
|
||||
filename_suffix: Optional[str] = None, _attempt: int = 1) -> bool:
|
||||
|
||||
@@ -149,6 +149,9 @@ class InstaloaderContext:
|
||||
if repeat_at_end:
|
||||
self.error_log.append(msg)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<InstaloaderContext is_logged_in={self.is_logged_in}>"
|
||||
|
||||
@property
|
||||
def has_stored_errors(self) -> bool:
|
||||
"""Returns whether any error has been reported and stored to be repeated at program termination.
|
||||
|
||||
@@ -18,8 +18,8 @@ def get_version():
|
||||
raise SystemExit("Could not find version string.")
|
||||
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
sys.exit('Instaloader requires Python >= 3.9.')
|
||||
if sys.version_info < (3, 10):
|
||||
sys.exit('Instaloader requires Python >= 3.10.')
|
||||
|
||||
requirements = ['requests>=2.25']
|
||||
optional_requirements = {
|
||||
@@ -46,7 +46,7 @@ setup(
|
||||
'from Instagram.',
|
||||
long_description=open(os.path.join(SRC, 'README.rst')).read(),
|
||||
install_requires=requirements,
|
||||
python_requires='>=3.9',
|
||||
python_requires='>=3.10',
|
||||
extras_require=optional_requirements,
|
||||
entry_points={'console_scripts': ['instaloader=instaloader.__main__:main']},
|
||||
zip_safe=False,
|
||||
@@ -58,12 +58,12 @@ setup(
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
'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.14',
|
||||
'Programming Language :: Python :: 3.15',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Topic :: Internet',
|
||||
'Topic :: Multimedia :: Graphics'
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Instaloader Tests
|
||||
|
||||
Integration tests for Instaloader features that require Instagram access are maintained here:
|
||||
https://github.com/instaloader/instaloader-tests
|
||||
@@ -1,237 +0,0 @@
|
||||
"""Unit Tests for Instaloader"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from itertools import islice
|
||||
from typing import Optional
|
||||
|
||||
import instaloader
|
||||
|
||||
PROFILE_WITH_HIGHLIGHTS = 325732271
|
||||
PUBLIC_PROFILE = "selenagomez"
|
||||
PUBLIC_PROFILE_ID = 460563723
|
||||
PUBLIC_PROFILE_WITH_IGTV = "natgeo"
|
||||
HASHTAG = "kitten"
|
||||
LOCATION = "362629379"
|
||||
OWN_USERNAME = "aandergr"
|
||||
NORMAL_MAX_COUNT = 2
|
||||
PAGING_MAX_COUNT = 15
|
||||
PRIVATE_PROFILE = "aandergr"
|
||||
PRIVATE_PROFILE_ID = 1706625676
|
||||
EMPTY_PROFILE = "not_public"
|
||||
EMPTY_PROFILE_ID = 1928659031
|
||||
|
||||
ratecontroller: Optional[instaloader.RateController] = None
|
||||
|
||||
|
||||
class TestInstaloaderAnonymously(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.dir = tempfile.mkdtemp()
|
||||
print("Testing in {}".format(self.dir))
|
||||
os.chdir(self.dir)
|
||||
self.L = instaloader.Instaloader(download_geotags=True,
|
||||
download_comments=True,
|
||||
save_metadata=True)
|
||||
self.L.context.raise_all_errors = True
|
||||
if ratecontroller is not None:
|
||||
# pylint:disable=protected-access
|
||||
ratecontroller._context = self.L.context
|
||||
self.L.context._rate_controller = ratecontroller
|
||||
|
||||
def tearDown(self):
|
||||
# pylint:disable=global-statement,protected-access
|
||||
global ratecontroller
|
||||
ratecontroller = self.L.context._rate_controller
|
||||
self.L.close()
|
||||
os.chdir('/')
|
||||
print("Removing {}".format(self.dir))
|
||||
shutil.rmtree(self.dir)
|
||||
|
||||
def post_paging_test(self, iterator):
|
||||
previous_post = None
|
||||
for post in islice(iterator, PAGING_MAX_COUNT):
|
||||
print(post)
|
||||
if previous_post:
|
||||
self.assertTrue(post.date_utc < previous_post.date_utc)
|
||||
previous_post = post
|
||||
|
||||
@unittest.skip('')
|
||||
def test_public_profile_download(self):
|
||||
profiles = {self.L.check_profile_id(PUBLIC_PROFILE)}
|
||||
self.L.download_profiles(profiles, profile_pic=False, fast_update=True, raise_errors=True)
|
||||
self.L.download_profiles(profiles, profile_pic=False, fast_update=True, raise_errors=True)
|
||||
|
||||
def test_public_profile_paging(self):
|
||||
self.post_paging_test(instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).get_posts())
|
||||
|
||||
def test_profile_pic_download(self):
|
||||
self.L.download_profiles({self.L.check_profile_id(PUBLIC_PROFILE)}, posts=False, raise_errors=True)
|
||||
|
||||
def test_hashtag_download(self):
|
||||
self.L.download_hashtag(HASHTAG, NORMAL_MAX_COUNT)
|
||||
|
||||
def test_hashtag_paging(self):
|
||||
for count, post in enumerate(instaloader.Hashtag.from_name(self.L.context, HASHTAG).get_all_posts()):
|
||||
print(post)
|
||||
if count == PAGING_MAX_COUNT:
|
||||
break
|
||||
|
||||
def test_get_id_by_username(self):
|
||||
self.assertEqual(PUBLIC_PROFILE_ID,
|
||||
instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).userid)
|
||||
|
||||
def test_get_username_by_id_private(self):
|
||||
self.assertEqual(PRIVATE_PROFILE.lower(),
|
||||
instaloader.Profile.from_id(self.L.context, PRIVATE_PROFILE_ID).username)
|
||||
|
||||
def test_get_username_by_id_public(self):
|
||||
self.assertEqual(PUBLIC_PROFILE.lower(),
|
||||
instaloader.Profile.from_id(self.L.context, PUBLIC_PROFILE_ID).username)
|
||||
|
||||
def test_get_username_by_id_empty(self):
|
||||
self.assertEqual(EMPTY_PROFILE.lower(),
|
||||
instaloader.Profile.from_id(self.L.context, EMPTY_PROFILE_ID).username)
|
||||
|
||||
def test_get_username_by_name_empty(self):
|
||||
self.assertEqual(EMPTY_PROFILE_ID,
|
||||
instaloader.Profile.from_username(self.L.context, EMPTY_PROFILE).userid)
|
||||
|
||||
def test_post_from_mediaid(self):
|
||||
for post in instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).get_posts():
|
||||
post2 = instaloader.Post.from_mediaid(self.L.context, post.mediaid)
|
||||
self.assertEqual(post, post2)
|
||||
break
|
||||
|
||||
def test_public_profile_tagged_paging(self):
|
||||
for post in islice(instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE).get_tagged_posts(),
|
||||
PAGING_MAX_COUNT):
|
||||
print(post)
|
||||
|
||||
def test_public_profile_igtv(self):
|
||||
for post in islice(instaloader.Profile.from_username(self.L.context, PUBLIC_PROFILE_WITH_IGTV).get_igtv_posts(),
|
||||
PAGING_MAX_COUNT):
|
||||
print(post)
|
||||
|
||||
|
||||
class TestInstaloaderLoggedIn(TestInstaloaderAnonymously):
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.L.load_session_from_file(OWN_USERNAME)
|
||||
|
||||
@unittest.skip('')
|
||||
def test_stories_download(self):
|
||||
self.L.download_stories()
|
||||
|
||||
@unittest.skip('')
|
||||
def test_private_profile_download(self):
|
||||
self.L.download_profiles({self.L.check_profile_id(PRIVATE_PROFILE)}, stories=True, raise_errors=True)
|
||||
|
||||
def test_stories_paging(self):
|
||||
for user_story in self.L.get_stories():
|
||||
print("profile {}.".format(user_story.owner_username))
|
||||
for item in user_story.get_items():
|
||||
print(item)
|
||||
|
||||
def test_highlights_paging(self):
|
||||
for user_highlight in self.L.get_highlights(PROFILE_WITH_HIGHLIGHTS):
|
||||
print("Retrieving {} highlights \"{}\" from profile {}".format(user_highlight.itemcount,
|
||||
user_highlight.title,
|
||||
user_highlight.owner_username))
|
||||
for item in user_highlight.get_items():
|
||||
print(item)
|
||||
|
||||
def test_private_profile_paging(self):
|
||||
self.post_paging_test(instaloader.Profile.from_username(self.L.context, PRIVATE_PROFILE).get_posts())
|
||||
|
||||
def test_feed_download(self):
|
||||
self.L.download_feed_posts(NORMAL_MAX_COUNT)
|
||||
|
||||
def test_feed_paging(self):
|
||||
for count, post in enumerate(self.L.get_feed_posts()):
|
||||
print(post)
|
||||
if count == PAGING_MAX_COUNT:
|
||||
break
|
||||
|
||||
def test_saved_download(self):
|
||||
self.L.download_saved_posts(NORMAL_MAX_COUNT)
|
||||
|
||||
def test_saved_paging(self):
|
||||
for count, post in enumerate(instaloader.Profile.from_username(self.L.context, OWN_USERNAME).get_saved_posts()):
|
||||
print(post)
|
||||
if count == PAGING_MAX_COUNT:
|
||||
break
|
||||
|
||||
def test_test_login(self):
|
||||
self.assertEqual(OWN_USERNAME, self.L.test_login())
|
||||
|
||||
def test_followees_and_stories(self):
|
||||
profile = instaloader.Profile.from_username(self.L.context, OWN_USERNAME)
|
||||
followees = set(islice(profile.get_followees(), PAGING_MAX_COUNT))
|
||||
self.L.download_profiles(followees, profile_pic=False, stories=True, posts=False, raise_errors=True)
|
||||
|
||||
def test_get_followees(self):
|
||||
profile = instaloader.Profile.from_username(self.L.context, OWN_USERNAME)
|
||||
for f in profile.get_followees():
|
||||
print(f.username)
|
||||
|
||||
def test_get_followers(self):
|
||||
profile = instaloader.Profile.from_username(self.L.context, OWN_USERNAME)
|
||||
for f in profile.get_followers():
|
||||
print(f.username)
|
||||
|
||||
def test_get_similar_accounts(self):
|
||||
profile = instaloader.Profile.from_username(self.L.context, OWN_USERNAME)
|
||||
for f in profile.get_similar_accounts():
|
||||
print(f.username)
|
||||
|
||||
def test_get_likes(self):
|
||||
for post in instaloader.Profile.from_username(self.L.context, OWN_USERNAME).get_posts():
|
||||
for like in post.get_likes():
|
||||
print(like.username)
|
||||
break
|
||||
|
||||
def test_explore_paging(self):
|
||||
for count, post in enumerate(self.L.get_explore_posts()):
|
||||
print(post)
|
||||
if count == PAGING_MAX_COUNT:
|
||||
break
|
||||
|
||||
def test_location_download(self):
|
||||
self.L.download_location(LOCATION, NORMAL_MAX_COUNT)
|
||||
|
||||
def test_location_paging(self):
|
||||
for count, post in enumerate(self.L.get_location_posts(LOCATION)):
|
||||
print(post)
|
||||
if count == PAGING_MAX_COUNT:
|
||||
break
|
||||
|
||||
|
||||
class TestCaptionMentions(unittest.TestCase):
|
||||
"""Caption parsing, which requires no network access."""
|
||||
|
||||
@staticmethod
|
||||
def _post_with_caption(caption: str) -> instaloader.Post:
|
||||
return instaloader.Post(None, {"shortcode": "BhrEy4nBFVU", "caption": caption})
|
||||
|
||||
def test_caption_mentions(self):
|
||||
for caption, expected in [
|
||||
("@alice at the beginning", ["alice"]),
|
||||
("preceded by a space @bob", ["bob"]),
|
||||
("preceded by a newline\n@carol", ["carol"]),
|
||||
("multiple lines\n@dave\n@erin", ["dave", "erin"]),
|
||||
("no mentions here", []),
|
||||
]:
|
||||
with self.subTest(caption=caption):
|
||||
self.assertEqual(self._post_with_caption(caption).caption_mentions, expected)
|
||||
|
||||
def test_caption_mentions_ignores_email_addresses(self):
|
||||
post = self._post_with_caption("write to alice@example.com\ncontact @bob instead")
|
||||
self.assertEqual(post.caption_mentions, ["bob"])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user