diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index f3ac5be..4433b5a 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -8,13 +8,14 @@ jobs: runs-on: ubuntu-latest steps: - name: "Checkout Instaloader Repository" - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 0 # needed for building docs - name: "Setup Python" - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: 3.12 + cache: 'pipenv' - name: "Install Dependencies" run: | python -m pip install pipenv==2023.12.1 @@ -23,7 +24,7 @@ jobs: 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 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e3c65e2..215f17a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -12,11 +12,12 @@ jobs: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - name: Checkout Instaloader Repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + cache: 'pipenv' - name: Install Dependencies run: | python -m pip install pipenv==2023.12.1 diff --git a/.github/workflows/packages.yml b/.github/workflows/packages.yml index e7e8217..25f775d 100644 --- a/.github/workflows/packages.yml +++ b/.github/workflows/packages.yml @@ -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 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 7c88c0f..3b81dd7 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -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 diff --git a/.github/workflows/windows_exe.yml b/.github/workflows/windows_exe.yml index c0d3b53..3704347 100644 --- a/.github/workflows/windows_exe.yml +++ b/.github/workflows/windows_exe.yml @@ -8,17 +8,18 @@ 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: diff --git a/docs/basic-usage.rst b/docs/basic-usage.rst index 11a2b25..7e349bb 100644 --- a/docs/basic-usage.rst +++ b/docs/basic-usage.rst @@ -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`. If an already-downloaded profile has been renamed, Instaloader automatically finds it by its unique ID and renames the folder accordingly. @@ -102,23 +102,23 @@ 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`. .. versionadded:: 4.2 - ``:stories`` The currently-visible **stories** of your followees (requires - :option:`--login`). + :ref:`login`). - ``:feed`` - Your **feed** (requires :option:`--login`). + Your **feed** (requires :ref:`login`). - ``:saved`` - Posts which are marked as **saved** (requires :option:`--login`). + Posts which are marked as **saved** (requires :ref:`login`). - ``@profile`` All profiles that are followed by ``profile``, i.e. the *followees* of - ``profile`` (requires :option:`--login`). + ``profile`` (requires :ref:`login`). - ``-post`` Replace **post** with the post's shortcode to download single post. Must be preceded by ``--`` in @@ -140,7 +140,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`), For a reference of all supported command line options, see :ref:`command-line-options`. @@ -255,7 +255,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`) 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,7 +307,7 @@ the post's caption:: instaloader --post-metadata-txt="{likes} likes." -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:: diff --git a/docs/cli-options.rst b/docs/cli-options.rst index 12b9218..8354f81 100644 --- a/docs/cli-options.rst +++ b/docs/cli-options.rst @@ -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`. .. 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`. .. 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`. .. option:: --highlights Also download highlights of each profile that is downloaded. Requires - :option:`--login`. + :ref:`login`. .. versionadded:: 4.1 @@ -183,6 +183,7 @@ Which Posts to Download Do not attempt to download more than COUNT posts. Applies to ``#hashtag``, ``%location_id``, ``:feed``, and ``:saved``. +.. _login: Login (Download Private Profiles) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -192,6 +193,9 @@ profiles. To login, pass the :option:`--login` option. Your session cookie (not password!) will be saved to a local file to be reused next time you want Instaloader to login. +Instead of :option:`--login`, it is possible to use +:option:`--load-cookies` to import a session from a browser. + .. option:: --login YOUR-USERNAME, -l YOUR-USERNAME Login name (profile name) for your Instagram account. @@ -204,8 +208,8 @@ Instaloader to login. 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. - After loading the cookies run the :option:`--login` option as it is required to download high quality media - and to make full use of Instaloader's features. + In subsequent runs, you can just use :option:`--login` to reuse the + same session, which is saved by Instaloader. .. versionadded:: 4.11 diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index a30ff5e..e5d9541 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -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` do not seem to be affected. Instaloader allows to adjust the rate controlling behavior by overriding :class:`instaloader.RateController`. diff --git a/instaloader/__init__.py b/instaloader/__init__.py index 0953bb0..bd3af02 100644 --- a/instaloader/__init__.py +++ b/instaloader/__init__.py @@ -1,7 +1,7 @@ """Download pictures (or videos) along with their captions and other metadata from Instagram.""" -__version__ = '4.11' +__version__ = '4.11.1' try: diff --git a/instaloader/__main__.py b/instaloader/__main__.py index 4bab104..6e18e26 100644 --- a/instaloader/__main__.py +++ b/instaloader/__main__.py @@ -5,7 +5,6 @@ import datetime import os import re import sys -import textwrap from argparse import ArgumentParser, ArgumentTypeError, SUPPRESS from enum import IntEnum from typing import List, Optional @@ -130,9 +129,7 @@ def import_session(browser, instaloader, cookiefile): raise LoginException(f"Not logged in. Are you logged in successfully in {browser}?") instaloader.context.username = username print(f"{username} has been successfully logged in.") - next_step_text = (f"Next: Run instaloader --login={username} as it is required to download high quality media " - "and to make full use of instaloader's features.") - print(textwrap.fill(next_step_text)) + print(f"Next time use --login={username} to reuse the same session.") def _main(instaloader: Instaloader, targetlist: List[str], @@ -204,7 +201,7 @@ def _main(instaloader: Instaloader, targetlist: List[str], 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() @@ -299,7 +296,7 @@ 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.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, @@ -346,17 +343,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]", @@ -387,11 +384,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', @@ -405,11 +402,11 @@ 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('--igtv', action='store_true', @@ -441,7 +438,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', @@ -508,8 +506,8 @@ 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 InvalidArgumentException() diff --git a/instaloader/instaloader.py b/instaloader/instaloader.py index caf6bca..062c983 100644 --- a/instaloader/instaloader.py +++ b/instaloader/instaloader.py @@ -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 @@ -1465,7 +1465,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): diff --git a/instaloader/instaloadercontext.py b/instaloader/instaloadercontext.py index b6be8df..b069642 100644 --- a/instaloader/instaloadercontext.py +++ b/instaloader/instaloadercontext.py @@ -305,9 +305,10 @@ 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 LoginException("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 LoginException("Login error: \"{}\" status, message \"{}\".".format(resp_json['status'], @@ -425,7 +426,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)): diff --git a/instaloader/structures.py b/instaloader/structures.py index b99ca87..3670df4 100644 --- a/instaloader/structures.py +++ b/instaloader/structures.py @@ -330,7 +330,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] @@ -685,6 +685,9 @@ class Post: .. versionchanged:: 4.7 Change return type to ``Iterable``. """ + if not self._context.is_logged_in: + raise LoginRequiredException("Login required to access comments of a post.") + def _postcommentanswer(node): return PostCommentAnswer(id=int(node['id']), created_at_utc=datetime.utcfromtimestamp(node['created_at']), @@ -749,7 +752,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 @@ -926,7 +929,7 @@ class Profile: .. versionadded:: 4.5.2""" if not context.is_logged_in: - raise LoginRequiredException("--login required to access own profile.") + raise LoginRequiredException("Login required to access own profile.") return cls(context, context.graphql_query("d6f4427fbe92d846298cf93df0b937d3", {})["data"]["user"]) def _asdict(self): @@ -980,7 +983,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'] @@ -1187,7 +1190,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, @@ -1247,7 +1250,7 @@ class Profile: .. versionadded:: 4.10 """ if not self._context.is_logged_in: - raise LoginRequiredException("--login required to get a profile's followers.") + raise LoginRequiredException("Login required to get a profile's followers.") self._obtain_metadata() return NodeIterator( self._context, @@ -1266,7 +1269,7 @@ class Profile: :rtype: NodeIterator[Profile] """ if not self._context.is_logged_in: - raise LoginRequiredException("--login required to get a profile's followers.") + raise LoginRequiredException("Login required to get a profile's followers.") self._obtain_metadata() return NodeIterator( self._context, @@ -1285,7 +1288,7 @@ class Profile: :rtype: NodeIterator[Profile] """ if not self._context.is_logged_in: - raise LoginRequiredException("--login required to get a profile's followees.") + raise LoginRequiredException("Login required to get a profile's followees.") self._obtain_metadata() return NodeIterator( self._context, @@ -1304,7 +1307,7 @@ class Profile: .. versionadded:: 4.4 """ if not self._context.is_logged_in: - raise LoginRequiredException("--login required to get a profile's similar accounts.") + raise LoginRequiredException("Login required to get a profile's similar accounts.") self._obtain_metadata() yield from (Profile(self._context, edge["node"]) for edge in self._context.graphql_query("ad99dd9d3646cc3c0dda65debcd266a7", @@ -1383,7 +1386,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={}