Compare commits

...
899 Commits
Author SHA1 Message Date
Alexander Graf 7efa2de6fe Fix get_feed_posts() with the api/v1/feed/timeline endpoint
The GraphQL query used so far now returns edge_web_feed_timeline: null, which
raised a TypeError. Page through api/v1/feed/timeline/ instead and convert its
media items (iPhone format).

Fixes #2556
2026-10-14 22:03:07 +02:00
André Koch-Kramer 58c07c6393 Include licenses in windows exe zip file 2026-10-13 21:33:05 +02:00
Alexander Graf bc859a1595 Release of version 4.15.4 2026-10-12 22:00:00 +02:00
Alexander Graf b5e50aa260 Update contributing guidelines 2026-10-12 21:54:36 +02:00
Markus Magnuson 0cf00a6079 Fix profile resolution and logged-in post fetching (#2765)
- `InstaloaderContext.get_page_data()` loads a page as a logged-out visitor and returns the query results that the page embeds. Instagram only embeds them when the request looks like a page navigation, so the request sends the `Sec-Fetch-*` headers of one.
- `Profile.from_username()` reads the profile from its public page. Anonymously, `_normalize_profile_data()` completes the node.
- Logged in, `Profile.get_posts()` uses query `28975909992013618` (`PolarisProfilePostsTabContentQuery_connection`) with the variables that the web app sends. The response has the same shape as before.

When logged in, `from_username()` does not normalize the page's node. The page shows the profile to a logged-out visitor, so normalizing would set `followed_by_viewer` to `False`, and `download_profiles()` would refuse private profiles that the session follows. Instead, the GraphQL profile query in `_obtain_metadata()` fills in the missing fields when they are first read.
2026-10-12 21:39:03 +02:00
Alexander Graf 91e30cf33b Support Python 3.15 2026-10-11 20:01:02 +02:00
Alexander Graf da65e697a0 Update dependencies 2026-10-10 20:53:07 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 132e2fa668 Bump actions/stale from 10 to 11 (#2751)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-10 20:26:16 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> e3aef9dc2b Bump pyinstaller from 6.16.0 to 6.22.3 (#2755)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-10 20:13:48 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> dd90abdb91 Bump pygments from 2.19.2 to 2.20.0 (#2758)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-10 20:06:46 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 2e3c89dfe9 Bump requests from 2.32.5 to 2.34.2 (#2756)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-10 19:59:07 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> c4434125e8 Bump actions/setup-python from 6 to 7 (#2752)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-10 19:56:46 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 75173094d6 Bump mypy from 1.18.2 to 2.4.0 (#2754)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-10 19:50:42 +02:00
Alexander Graf 559c7a9adf Drop Python 3.9 support, update Pipenv version used in CI 2026-10-10 19:45:43 +02:00
Andreas FehlnerandAlexander Graf 1e58dddb1f chore: add monthly Dependabot and harden checkout (#2713)
## What this changes
This PR adds Dependabot configuration and hardens GitHub Actions
checkout behavior.

## Dependabot
- adds .github/dependabot.yml
- enables grouped dependency updates
- schedules updates monthly
- configures a 7-day cooldown to avoid rapid follow-up bumps
- includes GitHub Actions updates

Configured update scopes:
- pip in /
- github-actions in /

## GitHub Actions hardening
Sets persist-credentials: false for ctions/checkout@v4 in:
- .github/workflows/windows_exe.yml
- .github/workflows/packages.yml
- .github/workflows/lint.yml
- .github/workflows/documentation.yml

## Why
- grouped updates reduce PR noise
- monthly cadence is lower maintenance
- cooldown helps avoid update churn
- disabling persisted checkout credentials follows least-privilege CI
practices

---------

Co-authored-by: Alexander Graf <17130992+aandergr@users.noreply.github.com>
2026-10-10 19:29:14 +02:00
Alexander Graf af898e2c31 Reference new integration tests and remove deprecated instaloader_unittests 2026-10-10 19:09:30 +02:00
Alexander Graf 8f680ae52d Add __repr__ to Instaloader and InstaloaderContext 2026-10-10 18:39:15 +02:00
agustux 7efc78de12 docs/codesnippets: Fix Firefox 147+ Linux cookie path (#2727) 2026-09-06 20:20:51 +02:00
Alexander Graf 543469296e Release of version 4.15.3 2026-07-26 11:01:52 +02:00
Elias 6dd77e7b56 Fix Post metadata fetching (#2706)
Use doc_id 27128499623469141 (PolarisPostRootQuery /
xdt_api__v1__media__shortcode__web_info) which returns v1/iPhone API format. Converts the new format to the legacy GraphQL field names that the rest of the codebase expects.
2026-07-26 10:47:46 +02:00
Markus Magnuson a4adeb0835 Fix profile resolution (#2701)
Fix Profile.from_id(), Profile.from_username() and Profile._obtain_metadata() by using `users/web_profile_info/` and `users/{profile_id}/info` queries.
2026-07-26 10:18:00 +02:00
Joseph May ad03b392db Fix caption mentions being missed after a newline
_mention_regex required the character before '@' to be a non-word
character other than '\n', so a mention at the start of a new line was
never matched. The '\n' exclusion was introduced in 8784ac7 when the
prefix went from '\W' to '[^\w\n]'; restoring '\W' matches mentions
after a newline while still keeping email addresses out of the results,
since the character before '@' in an address is a word character.

Also affects Profile.biography_mentions and StoryItem.caption_mentions,
which share the same regex.

Fixes #2709
2026-07-13 20:48:06 +02:00
André Koch-Kramer 297a88f252 Release of version 4.15.2 2026-07-05 21:16:53 +02:00
Mike Holzinger db2ae98928 Guard against null data in Post._obtain_metadata (#2705)
When Instagram returns {"data": null} for a shortcode metadata query (e.g. under
rate limiting, or for an unavailable post), response["data"]["xdt_shortcode_media"]
raised a raw TypeError before the existing `if pic_json is None` check could run.
Guard the intermediate "data" so the clean BadResponseException is raised instead,
which the download loop already handles.
2026-07-05 21:04:29 +02:00
npiriou b79a0a1e8b Fix profile metadata GraphQL query flags (#2696) 2026-07-05 20:49:03 +02:00
Alexander Graf d3cd639789 Remove unneeded symlink 2026-04-15 21:21:12 +02:00
Alexander Graf 618c23144e Welcome SocialAPIs as sponsor 2026-04-15 21:17:02 +02:00
Alexander Graf 66165a9d5d Improve typing on import_session() 2026-04-11 12:26:09 +02:00
Alexander Graf 27e42f661e Release of version 4.15.1 2026-03-21 22:01:12 +01:00
Xyfer 78bbd697cd Fix obtaining Profiles (when logged in) by migrating to new GraphQL endpoints (#2652)
Use doc_id 26347858941511777 (search query) in from_username() to get user_id
Use doc_id 25980296051578533 (PolarisProfilePageContentQuery) in _obtain_metadata()
Add _normalize_profile_data() to map new API fields to legacy format
2026-03-21 22:00:18 +01:00
Grant Nestor e4524e119a Save resume information on exception (#2600) 2026-01-18 16:46:03 +01:00
Alexander Graf 520b69d6ef Release of version 4.15 2025-11-08 22:27:37 +01:00
Alexander Graf 25c4daa861 Merge branch 'upcoming/v4.15' 2025-11-08 22:19:37 +01:00
Alexander Graf 7d256c36c1 Avoid using deprecated Hashtag.get_posts in codesnippets 2025-11-08 22:14:36 +01:00
Samuele MazziandAlexander Graf 64908788d3 Fix anonymous fetch of profile posts (#2533)
Co-authored-by: Alexander Graf <17130992+aandergr@users.noreply.github.com>
2025-11-08 22:07:58 +01:00
DenysandAlexander Graf b7ef306374 Abort download on {feedback,checkpoint,challenge}_required (#2612)
Handle Instagram feedback_required/checkpoint_required/challenge_required responses with a AbortDownloadException, causing the resumable iteration to be interrupted.

Co-authored-by: Alexander Graf <17130992+aandergr@users.noreply.github.com>
2025-11-08 18:10:47 +01:00
Alexander Graf 813621026c Update default user agent 2025-11-08 17:36:41 +01:00
Alexander Graf 44bd4d173b Update dependency versions used in CI 2025-11-08 17:33:16 +01:00
Naitri Doshi 2393a1a7a4 Add Post.video_play_count property (#2595) 2025-10-23 11:02:21 +00:00
Alexander Graf 5304e9794b First Alpha Release for Version 4.15 2025-07-18 08:06:07 +02:00
Alexander Graf ee3106814e Update default user agent 2025-07-18 08:05:11 +02:00
Alexander Graf 4c6d6e50af Update dependency versions used in CI 2025-07-18 08:05:11 +02:00
Alexander Graf af287c3fdd Merge branch 'master' into upcoming/v4.15 2025-07-18 07:51:08 +02:00
Alexander Graf 0bc9ab55dd Release of version 4.14.2 2025-07-18 07:47:19 +02:00
Ivan Semkin e64394fd25 Add possibly_pinned to download reels loop (#2539)
Add `possibly_pinned` to reels loop.
2025-07-04 07:45:07 +02:00
Alexander Graf 43cc15d1b3 Update paragraph explaining --load-cookies and --cookiefile 2025-01-29 08:09:38 +01:00
Alexander Graf a263ec85a6 Update default user agent 2025-01-29 07:52:35 +01:00
Alexander Graf 487d2c366d Update dependency versions used in CI
Changes to browser_cookie3: https://github.com/borisbabic/browser_cookie3/compare/0.19.1...0.20.1
2025-01-29 07:50:01 +01:00
master131 e493fa9c2d Update headers for iOS (#2472) 2025-01-24 08:23:49 +01:00
Alexander Graf 2e482b063a Release of version 4.14.1 2025-01-24 08:19:13 +01:00
Alexander Graf 0bc91b722e Fix download hashtags
Closes #2144.
2025-01-24 08:17:53 +01:00
tinyboxvk 4264d40f19 Fix typos (#2474)
Signed-off-by: tinyboxvk <13696594+tinyboxvk@users.noreply.github.com>
2025-01-15 08:29:32 +01:00
André Koch-Kramer b15a595083 Update .SRCINFO template for arch package 2024-11-21 08:41:30 +01:00
Alexander Graf 71ae32c85e Update PyPI and Arch packaging process 2024-11-21 08:26:05 +01:00
David Hotham f5134ff95e Publish wheels (#2430) 2024-11-21 07:58:27 +01:00
Alexander Graf 347f01b7fc Release of version 4.14 2024-11-07 08:21:03 +01:00
Alexander Graf 981eec9182 Third release candidate for version 4.14 2024-11-07 08:12:40 +01:00
Alexander Graf c54b61346e Merge branch 'master' into upcoming/v4.14 2024-11-07 08:12:00 +01:00
tinyboxvk fe9c8e1604 Bump pipenv, remove Python 3.8, add Python 3.13 (#2413) 2024-10-26 22:07:53 +02:00
tinyboxvk 524fb21b80 Update windows_exe.yml to address deprecation warnings (#2412) 2024-10-26 18:40:55 +02:00
Alexander Graf ed4add951f Second release candidate for version 4.14 2024-10-26 18:16:31 +02:00
Alexander Graf 8e6b2d83d3 Merge branch 'master' into upcoming/v4.14 2024-10-26 18:15:04 +02:00
Alexander Graf ba8faec6fe Release of version 4.13.2 2024-10-26 18:08:19 +02:00
Alexander Graf 9a74d9e109 Post._obtain_metadata: Use new XDT doc_id graphql query
Fixes #2384.
2024-10-26 18:08:19 +02:00
Alexander Graf 08f805482c First release candidate for version 4.14 2024-10-02 08:07:40 +02:00
Alexander Graf 308244b809 Merge branch 'master' into upcoming/v4.14 2024-10-02 08:06:35 +02:00
exwm b0c42a2662 feat: add --reels flag for downloading Reels videos from profiles (#2355) 2024-10-02 08:03:53 +02:00
Alexander Graf c5dacb5f6d Release of version 4.13.1 2024-08-28 08:29:57 +02:00
Eduardo Kalinowski 3a314de191 Remove rhx_gis header support (#2340)
The query used to obtain it does not work anymore.

The parameter was only used when obtaining a profile by its id, when
not logged in. This query is working fine without the special header,
so the whole functionality concerning rhx_gis was removed.

This also fixes this misleading messages logged when trying to obtain
a profile anonymously:
JSON Query to : Expecting value: line 1 column 1 (char 0)
2024-08-28 08:28:35 +02:00
Alex Chan 3a95250265 Clarify that post short codes should be preceded by a hyphen (#2343) 2024-08-28 08:27:58 +02:00
Yash Malik 2a3e5980ff minor doc update in basic-usage.rst (#2336)
(almost) nobody says "Id est". Even I, being a native English speaker, had to search for it. It should've been "i.e." in the first place. But, even that is not needed. Let's keep it simple.
2024-08-15 13:20:09 +02:00
Alexander Graf 8159229203 Update default user agent 2024-08-10 18:17:42 +02:00
Alexander Graf ae9a5efa85 Update dependencies 2024-08-10 18:17:42 +02:00
Alexander Graf e159065799 Release of Version 4.13 2024-08-10 17:36:41 +02:00
Eduardo Kalinowski ed21752365 Use new doc_id based query to get posts (#2330)
Fixes #2319.
2024-08-10 17:27:07 +02:00
Alexander Graf b0157d9bc1 Merge branch 'master' into upcoming/v4.13 2024-08-10 17:13:54 +02:00
Alexander Graf 754c3845d7 Release of Version 4.12.2 2024-08-10 17:00:58 +02:00
Caramel7 e3b8c38f8c Reduce GraphQL page length to 12 (#2332)
Fixes #2319.
2024-08-10 17:00:19 +02:00
Alexander Graf 7dac8ad1e5 Release of Version 4.12.1 2024-07-31 08:47:41 +02:00
Liponex b4cb9ea83a PEP 561 compliance enhancement (#2314) 2024-07-31 08:46:00 +02:00
Michel Ledig eb2accc088 feat: add max_count parameter to limit posts on download (#2276) 2024-07-17 08:03:57 +02:00
Seamus MullanandAlexander Graf 66c257362d Fix for issue #1085 (#2214)
Implement check for post.mediacount before checking if posts have been downloaded.

---------

Co-authored-by: Alexander Graf <17130992+aandergr@users.noreply.github.com>
2024-07-03 08:19:16 +02:00
Alexander Graf a0169b7094 Update default user agent 2024-06-29 19:48:19 +02:00
Alexander Graf 0a9fccaf26 Update dependencies 2024-06-29 19:48:19 +02:00
Alexander Graf 36d9f66e8e Release of Version 4.12 2024-06-29 19:20:24 +02:00
Alexander Graf 5e04063002 Merge branch 'master' into upcoming/v4.12 2024-06-29 19:19:30 +02:00
Alexander Graf 76e8154b58 Release of Version 4.11.1 2024-06-29 19:13:16 +02:00
Alexander Graf 4a969882b5 Produce more verbose HTTP error messages 2024-06-29 17:52:12 +02:00
Alexander Graf dc1eb2b193 Catch KeyboardInterrupt when asking for password 2024-05-30 10:22:45 +02:00
Alexander Graf 2a95589a1d Fix checkpoint URL output 2024-05-30 10:15:46 +02:00
Eduardo M KALINOWSKI 0c7398882b Detect errors when checking if the user is logged in 2024-05-30 08:04:51 +00:00
tinyboxvk 3b705fe5d1 Update actions versions (#2260)
Fix GitHub Actions Node.js deprecation warnings.

Use actions/setup-python cache where appropriate.
2024-05-20 20:07:44 +02:00
Alexander Graf 3ac790d30f Fix actions/stale 'leave open' handling 2024-05-20 09:50:48 +02:00
Alexander Graf fbfcd5df18 Close stale issues as "not planned"
Fixes #2168.
2024-05-18 16:32:35 +02:00
Alexander Graf b44a006344 Replace deprecated 'set-output' command in packages workflow 2024-05-18 16:17:59 +02:00
Eduardo Kalinowski 253144203b Download stories when --load-cookies is used (#2263) 2024-05-18 16:13:44 +02:00
Eduardo Kalinowski 7d0e46798e Add support for more browsers (#2258) 2024-05-18 16:11:12 +02:00
Alexander Graf fd8245a216 Fix workflow that deploys the documentation 2024-05-18 15:54:38 +02:00
tinyboxvk 3e335984f2 Replace deprecated 'set-output' command (#2261) 2024-05-18 15:44:36 +02:00
Hector Oliveros 98791fb0aa Add login required message when getting comments (#2237)
The comment function in the command line options documentation and the main program file has been edited to state that it now requires login. This change was crucial as the additional requests to Instagram server made for each post necessitate user authentication.
2024-05-12 18:16:36 +00:00
Eduardo Kalinowski 65b12d650c Use different exit codes for different failures (#2243) 2024-05-12 18:10:23 +00:00
Henry Mai 1fb98b066a docs: fix wording and grammar usage (#2203) 2024-03-29 10:20:13 +01:00
Alexander Graf 14f1c3cb82 Release of version 4.11 2024-03-18 08:08:09 +01:00
Alexander Graf 7a36512093 Third release candidate of version 4.11 2024-03-17 21:08:38 +01:00
Alexander Graf f423aa1507 Update dependencies 2024-03-17 21:04:56 +01:00
Alexander Graf 1875aede82 Merge branch 'master' into upcoming/v4.11 2024-03-17 20:49:37 +01:00
Alexander Graf 2cae933d66 Fix downloading comments
Closes issue #2125 by using an iphone endpoint for posts that have more
than 50 comments.
2024-03-17 20:42:26 +01:00
Alexander Graf 5509ea7daf Update sponsors in README 2024-01-12 08:29:09 +01:00
Alexander Graf 2b594909bb Release of version 4.10.3 2024-01-12 08:15:56 +01:00
Alexander Graf 1852447825 Fix downloading comments
Closes issue #2125 by using an iphone endpoint for posts that have more
than 50 comments.
2024-01-12 08:13:45 +01:00
Eduardo Kalinowski baaa2c2998 Add myself to AUTHORS.md (#2160) 2024-01-08 12:15:13 +01:00
Eduardo Kalinowski 47235bf2b8 Change pinned post detection, fixes #2142 (#2147)
The property that indicated whether a post is pinned is not available
anymore. As a workaround, the first five posts are treated differently
(as pinned posts were): if they were already downloaded, they are
skipped, but they do not cause the iteration to stop.
2024-01-08 07:46:27 +01:00
Alexander Graf 6eea8e6fcd Second release candidate of Version 4.11 2023-12-18 08:08:43 +01:00
Alexander Graf 5166fe691a Merge branch 'master' into upcoming/v4.11 2023-12-18 08:07:43 +01:00
Alexander Graf 9fa01509df Update pipfile.lock dependencies 2023-12-18 08:05:44 +01:00
Alexander Graf f0420e8a20 Update default user agent 2023-12-18 08:01:06 +01:00
Alexander Graf 7cc13ee08e Add versionadded directives and spacings 2023-12-18 07:58:53 +01:00
Juned Khan bf396f0e6c Implemented fix to avoid simuntaneous use of --login and --load-cookies (#2119) 2023-12-18 07:50:45 +01:00
Alexander Graf 39566735f7 Release of version 4.10.2 2023-12-09 17:59:08 +01:00
Alexander Graf fff25d6f4a Remove unnecessary do_sleep() 2023-12-09 17:58:40 +01:00
LostXOR b52547ea1c Fix 403 during login due to incorrect login endpoint and CSRF token location (#2133) 2023-12-09 17:56:21 +01:00
Alexander Graf 08bf0fef29 Attempt to fix publish package workflow 2023-11-15 08:02:57 +01:00
Alexander Graf 5efa5b3637 First release candidate of Version 4.11 2023-11-15 07:54:59 +01:00
Alexander Graf 82b46605a2 Add notice of 2FA currently not working 2023-11-15 07:53:45 +01:00
Alexander Graf f80eebec7a Update default user agent 2023-11-15 07:47:34 +01:00
Juned Khan 5a92f264c8 Load cookies from browser (#2040) 2023-11-15 07:32:18 +01:00
Alexander Graf acb65b5cca Merge branch 'master' into upcoming/v4.11 2023-10-16 08:54:59 +02:00
Alexander Graf 51a16a433e Release of version 4.10.1 2023-10-16 08:27:46 +02:00
Alexander Graf 6145c5b383 Temporarily disable lint check on python 3.11
It fails due to a problem with installing dependencies, which is already
fixed in a3dc75c04e.
2023-10-16 08:26:10 +02:00
LostXOR 8bc35cf3a1 Fixed error when trying to log in due to changes by Instagram (#2088) 2023-10-16 08:23:25 +02:00
Alexander Graf a3dc75c04e Update all dependencies 2023-10-14 17:31:41 +02:00
sczerniawski e48251e75e Fix crash related to --latest-stamps (#2056) 2023-09-18 08:15:35 +02:00
ybenel 3ebfe40729 Get comment likes (#1998) 2023-07-17 08:09:20 +02:00
Alexander Graf ebdc5f1557 Release of Version 4.10 2023-07-10 07:18:05 +02:00
Alexander Graf 4c6a184390 Merge branch 'master' into upcoming/v4.10 2023-07-10 07:15:36 +02:00
ybenel ed9ca62a48 Get Followed Hashtags (#1967) 2023-05-24 07:32:47 +02:00
Alexander Graf 99c3960ad4 NodeIterator: Support single page data 2023-05-15 07:37:34 +02:00
Eduardo Kalinowski 14124b617e Log the response when an abort status is received (#1901) 2023-05-10 07:19:53 +02:00
Alexander Graf b40a5aa243 resumable_iterator: catch EOFError when loading resume file
Fixes #1905.
2023-05-02 09:48:02 +02:00
Alexander Graf cc8c9ff883 README: Add new sponsor 2023-04-25 07:33:37 +02:00
Alexander Graf 75d3ab69d1 First Beta Release for Version 4.10 2023-04-19 07:58:14 +02:00
Alexander Graf 79820bb442 Merge branch 'master' into upcoming/v4.10 2023-04-08 07:29:13 +02:00
Alexander Graf f70a7eb4db README: remove travis badge 2023-04-02 12:48:40 +02:00
Eduardo Kalinowski 2dca99b89e Abort the download when redirected to login (#1900)
Abort the download when redirected to login (fixes #1882)
2023-03-30 18:51:27 +02:00
master131 1074a74a59 Update iOS version headers 2023-03-30 18:42:45 +02:00
Alexander Graf aa59683bd1 update CI dependencies, require Python>=3.8 2023-03-30 18:35:42 +02:00
Alexander Graf 6a3d763cf3 Release of Version 4.9.6 2023-02-11 13:21:07 +01:00
SayanthD e2d42807a2 Fixed downloading with location id 2023-02-11 13:13:27 +01:00
Eduardo M KALINOWSKI d07bd6a096 Detect redirects to https://i.instagram.com/accounts/login 2023-02-11 13:00:52 +01:00
Alexander Graf 0462d5c34a update CI dependencies, require Python>=3.8 2023-02-11 12:45:38 +01:00
master131 1db182ec27 Update UA and enhance headers for iOS struct (#1744) 2022-10-05 20:11:54 +02:00
Alexander Graf d09493e669 Merge branch 'master' into upcoming/v4.10 2022-10-05 20:02:51 +02:00
Alexander Graf 3cc29a4ceb Release of Version 4.9.5 2022-09-26 21:38:51 +02:00
ILikeToCode ffb0aea5eb Fixed downloading hashtags (#1749) 2022-09-26 21:37:02 +02:00
Alexander Graf 84bb60c155 Release of Version 4.9.4 2022-09-16 20:58:52 +02:00
fireattack 7857909886 Handle empty video resource in some old stories in highlights (#1732) 2022-09-16 20:56:01 +02:00
Alexander Graf 2206a7e5d7 Add new sponsor to README.rst
We are proud to announce @socialmethod as our new sponsor.
2022-09-16 19:03:24 +02:00
Alexander Graf f1b5e9cdf1 Increase stale period for pull requests and issues 2022-09-07 21:09:46 +02:00
Eduardo Kalinowski 8debcbb7ab Fixes incorrect latest stamp when download is resumed (#1717) 2022-09-07 21:01:48 +02:00
Alexander Graf e97fd8a27c Release of Version 4.9.3 2022-08-11 20:00:42 +02:00
fireattack 7f9d1618ad Use api/v1/feed/reels_media endpoint to get story info (#1675) 2022-08-11 19:59:02 +02:00
fireattack 6a42906697 don't try to call iPhone api when comparing profile_pic URL (#1681) 2022-08-11 19:56:52 +02:00
MiguelX413 0570dd71d6 Add Profile.biography_hashtags, .biography_mentions, StoryItem.caption, .caption_hashtags, .caption_mentions, and .pcaption 2022-07-23 16:49:36 +02:00
MiguelX413 fb14c2699e Make profile biographies be unicode normalized 2022-07-23 16:49:36 +02:00
MiguelX413 8784ac7d9b Fix and globalize regex 2022-07-23 16:49:36 +02:00
fireattack 4bb32e3c5a write caption in text mode (#1632) 2022-07-14 21:00:24 +02:00
Alexander Graf 5e232fd4f5 Release of Version 4.9.2 2022-07-04 20:18:22 +02:00
Eduardo Kalinowski da0dcb106c Handle pinned posts with --fast-update and --latest-stamps (#1586)
Instead of hard-coding a check for pinned items when storing the first
item, allows the creater of NodeIterator to specify how the comparison
should be made.

This also allows storing the newest post (whatever it's status is),
instead of simply storing the newst non-pinned post, which prevents
redownloads in case a user adds only a pinned post between runs.
2022-07-04 20:02:41 +02:00
MiguelX413 0c21da8c18 Improve typing syntax and type NamedTuples (#1610)
* Change to PEP 526 typing syntax

* Change from collections.namedtuple to typing.NamedTuple
2022-07-04 20:00:07 +02:00
Marco Volpato 9b928b4679 Add save and load session from dict (#1591) 2022-06-23 20:53:52 +02:00
Alexander Graf 8649a10113 docs: fix link to requests intersphinx inventory 2022-06-23 20:50:05 +02:00
Alexander Graf d406de4f35 docs: fix link to requests intersphinx inventory 2022-06-23 20:43:53 +02:00
Ziemowit Zabawa 2d37e1dca2 Make docs explicit about importing multiple argument files (#1548) 2022-06-16 10:25:43 +02:00
fireattack 37cbc5d72d Handle StoryImage in StoryItem.url (#1558) 2022-06-16 10:18:59 +02:00
Alexander Graf 862aa4e82c Merge branch 'master' into upcoming/v4.10 2022-06-08 18:18:53 +02:00
Alexander Graf 853e860363 Release of Version 4.9.1 2022-06-08 18:13:38 +02:00
twitter-79 90cfeaf471 Fixed fetching a users profile information (#1561)
* fixed fetching a users profile information

switched to new api/v1/users/web_profile_info/?username=xy endpoint

* fixed non existing users

thanks to @Dibort for fixing this!
2022-06-08 18:12:07 +02:00
Nicolai Weitkemper 5a24064012 Fix incorrect error messages when certain requests fail (#1536)
- handle case where `Profile.from_id` works, but `Profile.from_username` doesn't

- Handle case where `QueryReturnedNotFoundException` lies and `similar_profiles` contains the exact username

- Still raises `ProfileNotExistsException` for compatibility.
2022-05-30 21:29:34 +02:00
canh 644fab1f5e Make latest-stamps create intermediate directories (#1500)
Fixes #1467
2022-04-21 18:23:41 +02:00
MiguelX413 0704602e50 Checks if caption is not None before normalization (#1475) 2022-04-18 09:49:34 +02:00
Alexander Graf 218d88fc6d Release of Version 4.9 2022-03-26 17:18:56 +01:00
Alexander Graf 385c6c8a35 Merge branch 'upcoming/v4.9' 2022-03-26 17:15:09 +01:00
Alexander Graf c0e5d0475b StoryItem.from_mediaid()
Addresses #1238.
2022-03-26 17:03:57 +01:00
Alexander Graf fbd7df2a90 Use resumable iteration to download hashtags
Closes #1457.
2022-03-26 17:02:06 +01:00
JeremyKj87 569808f7a4 Fix for infinity loading if next page is empty (#1465) 2022-03-26 15:57:45 +01:00
Alexander Graf b808843efe Actions: Optimize Publish Package workflow 2022-03-19 17:27:22 +01:00
Alexander Graf f66cdd3e07 Release of Version 4.8.5 2022-03-19 17:15:52 +01:00
Alexander Graf 83dc5f749d Attempt to fix Documentation deployment 2022-03-19 17:02:24 +01:00
Alexander Graf 41de9e5a7c Attempt to fix AUR deployment 2022-03-19 16:53:55 +01:00
Alexander Graf d938f3e2f5 Third Beta Release for Version 4.9 2022-03-19 16:35:53 +01:00
Alexander Graf 374d9d0d81 Merge branch 'fully-migrate-to-gh-actions' into upcoming/v4.9 2022-03-19 16:34:59 +01:00
Alexander Graf 3cbee1b318 Fully Migrate to GitHub Actions 2022-03-19 16:29:48 +01:00
Alexander Graf dbc91cbeb1 Release of Version 4.8.5 2022-03-19 15:08:36 +01:00
fireattack 5a42a7537f Use header to determine file extension (#1447) 2022-03-19 15:04:42 +01:00
MiguelX413 d8d268f861 Mention regex will now be restricted to ASCII (#1456) 2022-03-18 14:57:28 +01:00
MiguelX413 463ada519e Add unicode normalization of captions (#1453) 2022-03-17 16:06:26 +01:00
canh 604b107586 Add --sanitize-paths option (#1452) 2022-03-17 15:27:36 +01:00
Alexander Graf 37a93ee59a Second Beta Release for Version 4.9 2022-01-15 17:34:18 +01:00
2d4f3b8537 Make {Post,StoryItem}.date_local timezone aware
This commit is made up of 21365ec668 555c86633c d864ce08ff

Discussed in #1305, #1316, #1372.

Co-Authored-By: Misael <misael.om@gmail.com>
Co-Authored-By: Eduardo Kalinowski <eduardo@kalinowski.com.br>
2022-01-15 17:34:18 +01:00
Alexander Graf 8c752532e1 Merge branch 'master' into upcoming/v4.9 2022-01-15 17:17:19 +01:00
Alexander Graf fdcf2e1367 Release of Version 4.8.4 2022-01-15 17:11:45 +01:00
Alexander Graf ca78fee307 Revert date_utc and date_local being timezone aware
Fixes #1379.

Revert "Make {Post,StoryItem}.date_utc timezone aware"
d864ce08ff.

Revert "Remove unnecessary conversion to timezone aware timestamp (#1372)"
555c86633c.

Revert "Timezone option for datetime style formatting (#1316)"
21365ec668.
2022-01-15 17:08:04 +01:00
Alexander Graf d6e5e31005 Release of Version 4.8.3 2022-01-07 14:56:40 +01:00
Alexander Graf d864ce08ff Make {Post,StoryItem}.date_utc timezone aware
Also simplify {Post,StoryItem}.date_local.

Discussed in #1316.
2022-01-07 14:53:23 +01:00
Eduardo Kalinowski 555c86633c Remove unnecessary conversion to timezone aware timestamp (#1372)
Post.date_local now already returns a timestamp with the appropriate
timezone.
2022-01-07 14:31:41 +01:00
Misael 21365ec668 Timezone option for datetime style formatting (#1316)
Add timezone awareness for datetime objects returned by `date_local` and `date_utc`.
Fixes #1305.
2021-12-31 16:50:16 +01:00
Alexander Graf 6343e9a69d Release of Version 4.8.2 2021-11-27 16:25:12 +01:00
Alexander Graf 06574eb428 Fix KeyError on attempt to get incomplete location
Fixes #1349.
2021-11-27 16:25:12 +01:00
JeremyKj87 815ba83d77 avoid comments endless loop - check if query_response is always the same (#1347) 2021-11-27 15:47:32 +01:00
Alexander Graf 5bff9b5474 First Beta Release for Version 4.9 2021-11-12 21:44:08 +01:00
Alexander Graf a23b832a69 Add Post.accessibility_caption property 2021-11-12 21:41:40 +01:00
Alexander Graf 5d18857695 Fix download of hashtags and locations
Fixes #1080, fixes #1129, closes #1240.
2021-11-12 21:33:48 +01:00
Alexander Graf d6fd4c560c Merge branch 'master' into upcoming/v4.9 2021-11-12 20:28:19 +01:00
fireattack 470fc0d905 Fix KeyError for video_url, and better workflow (#1321)
Fixes #1320.
2021-11-12 15:21:33 +01:00
Alexander Graf 0300d8a4f1 Fix CI for Python 3.10 2021-11-12 14:32:15 +01:00
Alexander Graf f09bd51ed4 Update feature suggestion issue template 2021-09-25 10:32:36 +02:00
Arman Yeghiazaryan 7a32272b55 Fix the issue with folder names starting with dots (#1252) 2021-08-14 16:27:14 +02:00
Alexander Graf 9eb2339028 Update default user agent string 2021-08-10 09:25:00 +02:00
Alexander Graf c45b536be9 Update dependencies 2021-08-10 09:25:00 +02:00
Alexander Graf 2c116683c2 Release of Version 4.8.1 2021-08-10 09:15:11 +02:00
Alexander Graf 054fa47245 Fix rare KeyError: 'video_versions' error
Closes #1251.
2021-08-10 09:13:42 +02:00
Alexander Graf 1f8ebe09b4 Release of Version 4.8 2021-08-04 18:27:45 +02:00
Alexander Graf 87dfdff335 Merge branch 'master' into upcoming/v4.8 2021-08-04 18:24:46 +02:00
Alexander Graf 631503d463 Log "Using latest stamps from ..." message
Instaloader always logs which files are loaded and considered in the
current run (such as session files or resume files). This commit adds a
log message "Using latest stamps from ..." when using --latest-stamps,
to be consistent.
2021-08-04 18:12:12 +02:00
Eduardo Kalinowski 43c9d32b22 Avoid uninitialized variable when called with :stories (#1242) 2021-08-04 18:04:16 +02:00
Alexander Graf 1ad9111753 Release of Version 4.7.6 2021-08-04 17:46:06 +02:00
fireattackandAlexander Graf 327fcfd8e8 Download best-quality video (#1232)
Co-authored-by: Alexander Graf <17130992+aandergr@users.noreply.github.com>
2021-08-04 17:42:14 +02:00
Arman Yeghiazaryan ae39ab9893 Fix Post.location / --geotags (#1244)
Closes #1109.
2021-08-04 17:28:56 +02:00
Alexander Graf 0372f7c8e8 Release of Version 4.7.5 2021-07-24 20:34:14 +02:00
Alexander Graf aaeaaa0e22 Fourth Release Candidate for Version 4.8 2021-07-24 19:29:56 +02:00
Alexander Graf 35bd8357d1 Merge branch 'master' into upcoming/v4.8 2021-07-24 19:28:53 +02:00
Eduardo Kalinowski 9cdf679fc1 Fix interruped downloads with --latest-stamps (#1219)
The most recent post is cached in NodeIterator (and
saved to the disk), and its timestamp is used, instead of the
timestamp instaloader was run.

This way, even in later resuming runs the timestamp stored is the same
that would have been stored if the first run.

Fixes #1206.
2021-07-24 19:27:46 +02:00
Alexander Graf 94d599db61 List all login issues in docs/troubleshooting 2021-07-24 19:18:39 +02:00
fireattack a0e3e8db0d Do not delete referer header (#1237)
since it causes IG's CDN to limit download speed
2021-07-24 16:37:07 +02:00
Alexander Graf 26c9f71132 Third Release Candidate for Version 4.8 2021-06-22 21:52:26 +02:00
Eduardo Kalinowski 210719af56 Avoid uninitialized variable (#1204)
fixes #1203
2021-06-22 21:50:06 +02:00
Alexander Graf 8c1129e556 Add GitHub Sponsors link 2021-06-20 13:50:59 +02:00
Alexander Graf c200031be6 Increase stale period for pull requests and issues 2021-06-20 13:50:59 +02:00
Alexander Graf a0d2ec95b9 Release of Version 4.7.4 2021-06-18 15:19:51 +02:00
Alexander Graf 750dd3f47f Second Release Candidate for Version 4.8 2021-06-18 15:13:01 +02:00
Alexander Graf 839b77eef0 First Release Candidate for Version 4.8 2021-06-16 23:17:04 +02:00
Alexander Graf e346842f7b Merge branch 'master' into upcoming/v4.8 2021-06-16 23:08:18 +02:00
Alexander Graf 9b3f809a9a Release of Version 4.7.3 2021-06-16 23:00:18 +02:00
twitter-79 803440ac50 Fix Login Error (#1192)
taking csrf token from login page as it is empty using the old url

Fixes #1150.
2021-06-16 22:35:58 +02:00
Eduardo Kalinowski 455a757159 Make downloads using --latest-stamps resumable (#1193)
Using itertools.takewhile() on a NodeIterator returns a plain Iterator,
and so it's not resumable.

The strategy has been altered to pass an extra argument to
posts_download_loop, a lambda that is evaluated for each post, and
causes the loop to stop when it returns false.
2021-06-16 22:19:16 +02:00
Alexander Graf 07de73bde1 Fix missing 2FA error message 2021-06-15 21:05:57 +02:00
spykard bf83d406d3 Fix and Update has_public_story Functionality (#1183) 2021-06-15 19:39:06 +02:00
Eduardo Kalinowski ac5d6e312a Report Profile renames even if --quiet is enabled (#1180)
While this isn't really an error, it requires user action, so the message
is printed to stderr even with --quiet, and repeated at the end when that
option is not enabled.

Resolves #1159.
2021-06-10 18:49:05 +02:00
Alexander Graf 3bb4a5b4b1 Release of Version 4.7.2 2021-06-06 11:18:48 +02:00
Kalle Westerling 9ffdf258a9 Fix Post.mentions including email addresses (#1167)
Earlier, if someone had an email address in the caption, those were provided as mentions in the Post.mentions attribute. With this update to the regular expression, email addresses are no longer ingested.

Fixes #1029.
2021-06-06 11:13:05 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> d530a7a37f Bump urllib3 from 1.26.4 to 1.26.5 (#1163)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.4 to 1.26.5.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.4...1.26.5)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-04 15:12:01 +02:00
Eduardo Kalinowski d142fb70b0 --latest-stamps, like --fast-update, but without the need to keep downloaded files (#1131)
Adds the --latest-stamps command line option, pointing to a file where
the latest time each profile was scraped. On the next run, only posts
newer than that time are downloaded.

Fixes #1122.
2021-05-27 18:22:33 +02:00
SAO 8a26943215 update documentation for --count option (#1139) 2021-05-17 18:51:39 +02:00
Eduardo Kalinowski 5345470ebf Add option --title-pattern for profile pics and covers (#1127) 2021-05-15 16:25:36 +02:00
Eduardo Kalinowski a2715cbe03 Show progress of profile scraping (#1114) 2021-04-29 21:14:23 +02:00
anokhovd fa19b95107 Add support for loading structures directly from python json objects (#1092) 2021-04-22 20:12:42 +02:00
A Bored Pervert 3837b642b0 Make iPhone endpoint query optional (#1076) 2021-04-22 20:06:35 +02:00
Alexander Graf e67b9e6c37 Improve docs on how to load/save FrozenNodeIterator 2021-04-12 16:54:44 +02:00
Alexander Graf 2bf175a111 Release of Version 4.7.1 2021-03-27 20:47:28 +01:00
Alexander Graf 95d8534aed Fix SRCINFO depends metadata in AUR 2021-03-27 20:46:53 +01:00
Alexander GrafandAndré Koch-Kramer b583cd07e1 Get better-quality version of pictures if logged-in
Fixes #1065 (also see #630).

Co-Authored-By: André Koch-Kramer <koch-kramer@web.de>
2021-03-27 20:42:56 +01:00
Alexander Graf 52f258e89b Release of Version 4.7 2021-03-27 15:19:12 +01:00
Alexander Graf e9888e72fd Second Release Candidate for Version 4.7 2021-03-20 17:44:04 +01:00
Alexander Graf c72b2a8fc2 Default download_comments and download_geotags to False 2021-03-20 17:39:37 +01:00
Alexander Graf e2cec0a0bf Update dependencies; test on py3.9; drop python 3.5 support 2021-03-20 17:30:15 +01:00
Alexander Graf 08eaa7d474 always keep exception chains 2021-03-20 17:17:08 +01:00
Alexander Graf f70355e803 Update default user agent 2021-03-20 17:15:05 +01:00
Alexander Graf bba7d5131c First Release Candidate for Version 4.7 2021-03-13 11:24:53 +01:00
Alexander GrafandAndré Koch-Kramer f6731566cd Support resuming of downloading comments
Co-Authored-By: André Koch-Kramer <koch-kramer@web.de>
2021-03-13 11:17:56 +01:00
Alexander Graf cd13211603 Minor optimization in get_sidecar_nodes() 2021-02-24 16:35:08 +01:00
Alexander Graf 8635a576f0 Fix line-too-long pylint complaint 2021-02-24 16:25:56 +01:00
Alexander Graf 8f594a3a30 Merge branch 'master' into upcoming/v4.7 2021-02-24 16:10:27 +01:00
Alexander Graf 0f11ef1bdd Release of version 4.6.2 2021-02-24 16:01:33 +01:00
Alexander Graf a4bb353421 docs: Enhance description of --resume-prefix 2021-02-24 16:01:22 +01:00
Alexander Graf dfdd744804 Minor fixes to top-level README.md 2021-02-24 16:00:28 +01:00
Thomas 9fb112faa8 Fix issue 892 Save content using original filenames (PR 893) (#1025)
- When a post contained sidecars, the filename for caption, json etc. was changed. It was no longer the original filename like before.
- If the post contains sidecars, a local variable is used to build the filenames for the sidecar media.
2021-02-20 10:17:57 +01:00
Alexander Graf f85e49d521 Spare unnecessary full metadata fetch
This fetch was done for sidecar posts that contain a video when going
through a profile. The fetched information is already present with the
new profile query introduced in the last commit, making this full
metadata fetch query unnecessary. Instaloader now better evaluates if
that fetch must be done or not.

The fetch was also (eventually unnecessarily) made when accessing
get_sidecar_posts() on a Post that has been loaded with
load_structure_from_file().
2021-02-17 16:40:54 +01:00
Thomas 6c4da39372 Update the query_hash for post downloads (#1026)
- It's necessary because for the old query_hash IG isn't returning the full structure any more.
- So the old query_hash is replaced with the current one.
2021-02-17 16:07:18 +01:00
Alexander Graf a2d756b177 Flag --abort-on to abort on given status codes
Closes #920.
2021-02-13 19:23:14 +01:00
Alexander Graf e15d67c065 Change login hint from "Warning" to "Hint" 2021-02-13 19:12:13 +01:00
Alexander Graf 640066dd60 Merge branch 'master' into upcoming/v4.7 2021-01-30 17:07:21 +01:00
Alexander GrafandAndré Koch-Kramer 672b0b1840 Fix efficiency bug regarding re-downloading targets
Remove unnecessary queries when a file in a target does not have to be
re-downloaded.

This significantly speeds up the download in cases where the majority of media
is already present locally.

Co-Authored-By: André Koch-Kramer <koch-kramer@web.de>
2021-01-30 16:54:35 +01:00
Alexander Graf 828fa71ede Release of version 4.6.1 2021-01-25 16:04:30 +01:00
Alexander Graf 4fd57f5598 Adjust user agent string and sleep behavior 2021-01-25 15:53:49 +01:00
Alexander Graf c5b686a213 Update RateController to current observations 2021-01-25 15:46:05 +01:00
43c521afa9 Add method to get title of post (#976)
Co-authored-by: Tejas Chauhan <tejas.chauhan@kevit.io>
Co-authored-by: Alexander Graf <17130992+aandergr@users.noreply.github.com>
2021-01-16 18:50:27 +01:00
André Koch-Kramer 3955540080 Release of version 4.6 2021-01-09 16:23:49 +01:00
André Koch-Kramer 3fe6b1e139 Merge remote-tracking branch 'origin/upcoming/v4.6' 2021-01-09 16:11:05 +01:00
Alexander Graf a2ce906fd0 Fix efficiency bug regarding {filename} in filename-pattern
Avoid evaluating 'url' when preparing the filename if the
filename-pattern does not contain the "{filename}" token. This avoids
the URL to be queried even though it is not needed (e.g. with
--no-pictures).
2021-01-04 17:37:19 +01:00
Alexander Graf 850cd39be4 First Release Candidate for Version 4.6 2021-01-02 18:42:49 +01:00
Alexander Graf 227efa7b68 Improvements to contributing.md, other minor changes 2021-01-02 17:41:19 +01:00
Alexander Graf d7f7cad4a2 docs: Link to examples in master branch 2020-12-23 19:39:18 +01:00
Alexander Graf 2f69ea62a1 First Alpha Release for Version 4.6 2020-12-14 22:16:22 +01:00
Alexander Graf 0f258b6118 Merge branch 'master' into upcoming/v4.6 2020-12-14 22:14:01 +01:00
AndyRandAlexander Graf b31f279527 Allow selecting range to download from an sidecar (#749)
Co-authored-by: Alexander Graf <17130992+aandergr@users.noreply.github.com>
2020-12-14 22:11:33 +01:00
Alexander GrafandAndré Koch-Kramer ed55ce140d Fix efficiency bug in download_profiles()
Fixes efficiency of the download_profiles() function when called with
--no-posts --no-profile-pic by reordering an if statement.

This inefficiency has been reported in #724.

Co-authored-by: André Koch-Kramer <koch-kramer@web.de>
2020-12-12 16:49:38 +01:00
Alexander Graf dcc7ab89c1 615_import_firefox_session.py: Open database with immutable=1
Allows to open the firefox cookie database even though Firefox is
holding a lock.

Fixes the "Cookie import failed: database is locked" message when using
the script while Firefox was running.

This has been reported in #877.
2020-12-06 08:43:04 +01:00
Alexander Graf 610756d342 Improvements to docs/contributing.rst 2020-12-04 17:08:42 +01:00
Alexander Graf 097bf7fecc Merge branch 'master' into upcoming/v4.6 2020-12-04 14:10:18 +01:00
Alexander Graf 3dcb0ff542 Release of Version 4.5.5 2020-12-04 13:28:53 +01:00
Alexander Graf 0be1129173 Fix login redirect triggering 429 handling
Closes #902.
2020-12-04 13:12:24 +01:00
Alexander Graf ace0e944ec docs: Fix description of Story.last_seen_utc 2020-12-02 19:33:13 +01:00
Alexander Graf 3530cdc55c Fix rare FileNotFoundError in resumable_iterator()
... that occured when using a path name for the FrozenNodeIterator file
whose dirname was ''.
2020-12-02 19:26:27 +01:00
Alexander Graf ed3fce45da More emphasis on documentation in --help output 2020-12-02 19:24:32 +01:00
Thomas a045168c79 Save content using original filenames (#893)
Add feature from issue #892 (Save content using original filenames)

- The new feature would download posts and save the images/videos with the original filenames.
- The implementation follows the other options for the filename-pattern parameter. As filename is related to a single file and not the whole post, for sidecar nodes the filename is calculated again.
- Using this option media from one post is only grouped together by the file modification timestamp.
2020-12-02 11:22:55 +01:00
Alexander Graf 4c02a186d3 Merge branch 'master' into upcoming/v4.6 2020-11-28 19:00:49 +01:00
Alexander Graf e9648bc551 Release of Version 4.5.4 2020-11-28 16:31:05 +01:00
Alexander Graf 6d90c47d9a Troubleshooting: Mention overriding RateController 2020-11-28 16:29:18 +01:00
Alexander Graf 1c577716dd Fix HTTP error 500 when accessing :stories in certain cases
Based on a comment in #795:
https://github.com/instaloader/instaloader/issues/795#issuecomment-726634493

Closes #795.
2020-11-28 16:15:51 +01:00
Alexander Graf 6824d7b75c Update year in LICENSE file 2020-11-28 16:13:10 +01:00
Alexander Graf 6ad03d3575 Link Insights -> Contributors in AUTHORS.md 2020-11-28 16:11:45 +01:00
Alexander Graf 03ac067e71 Add Contributor Covenant Code of Conduct 2020-11-28 16:08:31 +01:00
Rafael Ristovski e70faf506d Update graphql query hash used for stories (#872)
This updated graph query hash returns all fields for `tappable_objects`. These fields were missing/partial when using the old query hash.
This allows to get the full json metadata of a story.
2020-11-19 11:02:32 +01:00
Alexander Graf e11b88d44b Advertise PEP 561 compliance 2020-11-19 10:32:46 +01:00
Alexander Graf cc2986cff3 Fix Hashtag.get_all_posts() missing one post in certain cases
When a hashtag query returned only the 9 top posts, get_all_posts() yielded only 8 instead of all 9 posts.

Closes #853.
2020-11-14 20:09:24 +01:00
Alexander Graf c7372e36cc Require being logged in for Post.get_likes()
Closes #860.
2020-11-14 18:57:49 +01:00
Alexander Graf 1c2e226630 Enable default request timeout of 300 seconds (#870)
Sets a default request timeout of 300 seconds to fix Instaloader hanging indefinitely when used on an unstable internet connection without using --request-timeout, such as in #810.
2020-11-14 18:47:37 +01:00
Alexander Graf 74f7970b50 Fix a KeyError when fetching post metadata fails
Fixes #824.
2020-11-13 11:20:31 +01:00
André Koch-Kramer f4ecfea64c Add Android to docs installation page 2020-09-28 21:14:38 +02:00
Alexander Graf 0586a1fec3 Update docs/troubleshooting.rst 2020-09-19 17:07:57 +02:00
Ingo Marquardt c53625028d Avoid additional http request to get post's owner ID. (#809) 2020-09-18 11:23:06 +02:00
André Koch-Kramer 4ad4584b0d Release of Version 4.5.3 2020-09-03 20:23:57 +02:00
886530d661 Fix wrong ProfileNotExistsException in some edge cases (#785)
Fixes wrong "Profile does not exist" error.

Co-authored-by: André Koch-Kramer <koch-kramer@web.de>
Co-authored-by: Alexander Graf <17130992+aandergr@users.noreply.github.com>
2020-09-03 20:20:02 +02:00
Alexander Graf 574cadbb82 Adjust bug report template 2020-08-31 20:35:13 +02:00
Alexander Graf 6d3c73eec3 Adjust RateController to current observations 2020-08-31 20:29:21 +02:00
Alexander Graf 438669b954 Release of Version 4.5.2 2020-08-23 18:55:22 +02:00
Alexander Graf a7a05c200c Adjust RateController to current observations
Fixes #774.
2020-08-23 18:51:39 +02:00
Alexander Graf f53bdf9ef8 Do not call get_sidecar_nodes() if not needed
Do not call Post.get_sidecar_nodes() in download_post() if both --no-pictures
and --no-videos are set.
2020-08-23 18:48:21 +02:00
Alexander Graf 13612c606e Use more reliable query for own profile for :saved
Adds a method Profile.own_profile() which uses the test_login() query for
obtaining the own profile. This method is used for accesssing saved posts
(Profile.get_saved_posts() on own profile).

Also removes the now-unneeded self._obtain_metadata() call in
Profile.get_saved_posts(), and changes the NodeIterator such that data is never
None and the count property is more likely available.

Fixes #563.
2020-08-23 18:46:34 +02:00
fireattack b57bbe2af0 Get higher Post video and StoryItem image quality if logged-in (#712) 2020-08-21 19:37:38 +02:00
Alexander Graf 3a6ba6485e Release of Version 4.5.1 2020-08-20 11:08:08 +02:00
Alexander Graf 177a299ced Improve 429 error message
This text_for_429 error message is expected to be less confusing for new users.
Further improve the output formatting of _dump_query_timestamps().
2020-08-20 10:38:06 +02:00
Alexander Graf 4a1fe035af First Release Candidate for Version 4.5.1rc1 2020-08-17 18:29:37 +02:00
Alexander Graf db86f81be7 Fix support for Python 3.5 prior Python 3.5.4 (#777) 2020-08-17 18:25:22 +02:00
Alexander Graf 020d412342 Fix some video download edge cases (#776)
Fixes unintended sidecar video skipping if --no-pictures is set.

Fixes video download if video thumbnail access fails with 410 error.
2020-08-17 18:23:24 +02:00
lapourgagner 6f57197afb Fixed issue #769 by using os.replace instead os.rename (#775) 2020-08-17 18:17:23 +02:00
Alexander Graf 57393f619d doc: Don't force links to be opened in new tab 2020-08-09 08:08:17 +02:00
Alexander Graf d5dd8cec00 Fix Downloads/Month Badge
Change to pepy.tech as source for this badge.
2020-08-06 09:43:22 +02:00
Alexander Graf 3652e009d9 Release of Version 4.5 2020-08-02 10:32:44 +02:00
Alexander Graf ecde92d05e Minor fix of Hashtag.get_all_posts() docstring 2020-08-02 10:23:09 +02:00
Alexander Graf a8d3851956 Nicer syntax for resumable_iteration usage 2020-08-02 10:22:45 +02:00
Alexander Graf c71873231d Minor Documentation Fixes (mostly spelling&grammar) 2020-07-31 22:12:27 +02:00
Alexander Graf 3823b1e593 Documentation Template: Use ARIA roles
This fixes the full text functionality of the search.
2020-07-31 11:41:22 +02:00
Alexander Graf 6c779e8d4b First Beta Release for Version 4.5 2020-07-30 17:17:55 +02:00
Alexander Graf c24015a93d Merge branch 'master' into upcoming/v4.5 2020-07-30 17:16:00 +02:00
Alexander Graf 2c56e6994a Enhance Documentation Theme
Improvements on our bootstrap 4 theme for sphinx.
2020-07-30 17:02:43 +02:00
Alexander Graf 6791e9a2f8 Textual Improvements on Documentation
- Fix spelling errors / typos,

- Remove deprecated command line options from doc.
2020-07-30 16:41:45 +02:00
Alexander Graf 64652f3994 Feature Request template: PR -> pull request 2020-07-30 13:20:02 +02:00
Alexander Graf 4f5b223b60 Minor docs fixes 2020-07-26 19:00:01 +02:00
Alexander Graf 2a3711687f Update deps used in CI (esp. MyPy and PyLint) 2020-07-26 18:54:23 +02:00
Alexander Graf 612842f255 Minor nodeiterator-related docs improvements 2020-07-26 18:05:46 +02:00
Alexander Graf fe9cd653e7 docs: Improvements to branded syntax highlighting 2020-07-26 18:01:33 +02:00
Alexander Graf 82e7b19d6d docs: Split as-module into smaller documents 2020-07-26 18:00:37 +02:00
Alexander Graf 5d7c36399f Windows: Sanitize Path of Resume Files 2020-07-24 18:35:10 +02:00
Alexander Graf f0e66ef465 Windows EXE: Fix CommandNotFoundException
Fixes CommandNotFoundException that occured when started from within a
directory whose name contains a space.
2020-07-24 17:19:48 +02:00
Alexander Graf f71ea562af First Alpha Release for Version 4.5 2020-07-21 17:37:45 +02:00
Alexander Graf bc40b82f94 Resume a previously-aborted post download loop (#732)
With this change, Instaloader is capable of resuming a previously-aborted download loop. To do so, it creates a JSON file within the target directory when interrupted, that contains all the necessary information to later resume that operation.

Resuming an interrupted download is supported for most, but not all targets. It is supported for:

- Regular profile posts,
- IGTV posts
- Saved posts,
- Tagged posts,
- Explore posts.
2020-07-21 17:28:55 +02:00
Alexander Graf c817d1901a Merge branch 'master' into upcoming/v4.5 2020-07-18 15:32:05 +02:00
Alexander Graf e232c82d5e Release of Version 4.4.5 2020-07-18 15:24:31 +02:00
Alexander Graf 601dd0b106 First Release Candidate for Version 4.4.5 2020-07-15 10:26:22 +02:00
Alexander Graf 4c20b573e2 Fix KeyError on Posts with very few comments 2020-07-15 10:17:20 +02:00
Cadence EmberandAlexander Graf db63e5c7e5 Bypass IP-based redirects to /accounts/login (#727)
* Bypass IP-based redirects to /accounts/login

There are two main changes made:

For users, we request /{username}/feed/ instead of /{username}/. For some
reason, this completely bypasses the login redirect. This page doesn't
work in browser while blocked, but fortunately all the data we need is
just present in the HTML page.

For posts, we change from using the /p/ page to using the graphql
endpoint for the same data, which is still subject to graphql rate
limits, but is not subject to login redirects. The data is identical
between the two pages, apart from the object keys being sorted
differently and rhx_gis being missing on graphql.

Yes, this now unblocks access from VPNs, Tor, cloud servers, etc.

* Apply requested patch to fix comments

* Remove rhx_gis from Post and Profile

Co-authored-by: Alexander Graf <17130992+aandergr@users.noreply.github.com>
2020-07-15 09:46:39 +02:00
Alexander Graf fbe05a1add More flexible Rate Controller (#716)
Move InstaloaderContext's rate controlling logic into a class
RateController with fine-grained methods to enable easily changing
Instaloader's behavior regarding rate limits.
2020-07-11 13:54:31 +02:00
Alexander Graf e21d34124d Add codesnippet for almost chronological order
Such as for downloading hashtag feeds, as discussed in #666 and contributed by
@e2tovar.

Also change comment color to grey in codesnippets in documentation.
2020-06-21 19:38:16 +02:00
Lars Lindqvist fa62025ea3 Deprecate commit-mode (#697)
Rather than checking the json file to make sure posts have been
successfully downloaded, data is stored in a temporary file which
is renamed when downloading has finished, as suggested in #257.
2020-06-20 16:23:31 +02:00
michalens 66b3ec6928 Add explanation on downloading single post (#694) 2020-06-19 19:39:08 +02:00
Alexander Graf f7d82495bd Consistently use native '\' path separator on Windows
Use os.path.join() for joining paths rather than +'/'+.

os.path.join() uses '/' on Unix-ish and '\' on Windows. On Windows, this makes
the following improvements:

- Visual consistency with / and \ not being mixed-up in one path (os.path.join()
  has already been used in many places).

- Allow using the \\?\ bypass method, e.g. with
    --dirname-pattern=\\?\D:\{target}
  The \\?\ bypass method fails when '/' is used as path separator. With the \\?\
  bypass method, we can create directories whose name would otherwise be
  considered invalid, such as directories starting with "aux.".
2020-06-19 19:33:46 +02:00
Alexander Graf b0a430051e Restrict GH Action lint workflow on Python 3.8
Now that Travis-CI status reporting works again, there is no need to run the
tests twice.
2020-06-18 08:33:25 +02:00
Alexander Graf 8b56b90e72 Less intrusive PR template 2020-06-18 08:32:46 +02:00
Alexander Graf 0c93e63615 Update troubleshooting.rst, Minor fixes to contributing.rst 2020-06-16 21:44:22 +02:00
Alexander Graf 65025e61d9 Update issue/PR templates, contributing.rst, CI
- Keep documentation in sync with master branch

- Run PyLint and MyPy on Github Actions

- Update Issue templates, enforce usage of issue templates, add links to
  resources in Instaloader Documentation

- Update contributing.rst
2020-06-16 20:26:58 +02:00
Alexander Graf 11baf493db Release of Version 4.4.4 2020-06-11 20:10:52 +02:00
Alexander Graf 2345d90e0e Fix docs building on Windows 2020-06-09 13:03:48 +02:00
Alexander Graf 71b09aed0f Warn if --login parameter is not a username
Closes #624.
2020-06-09 11:44:06 +02:00
Alexander Graf fd6d8542c9 Add a unit test for an empty private profile 2020-06-09 11:44:06 +02:00
Alexander Graf df6fbbee76 Third Release Candidate for Version 4.4.4 2020-06-08 18:51:46 +02:00
Alexander Graf 1661bf7722 Remove unused import 2020-06-08 18:44:43 +02:00
Alexander Graf 8f9723cf62 Fix "Use --login ..." warning at startup 2020-06-08 18:40:13 +02:00
Alexander Graf e1567a0342 Second Release Candidate for Version 4.4.4 2020-06-08 18:22:06 +02:00
Alexander Graf 610b6242a0 Get higher image quality if logged-in
Addresses a suggestion in #630.
2020-06-08 17:53:39 +02:00
Alexander Graf d90f90988a Fix ValueError when executing from other drive on Windows
Fixes #671.
2020-06-08 17:01:42 +02:00
Alexander Graf d3c747a306 First Release Candidate for Version 4.4.4 2020-06-06 10:52:42 +02:00
Alexander Graf 225353dc21 Download highest-resolution images if logged-in (#662)
If logged-in, Post.url and Post.get_sidecar_nodes() now use the iPhone API
endpoint to obtain the highest-resolution image version. Resolves #630.

A notice "Warning: Use --login to download higher-quality versions of pictures"
is issued if the user tries to download posts without being logged-in.

Further, the message "Errors occurred:", which is displayed when the
InstaloaderContext is closed, has been changed to "Errors or warnings
occurred:".
2020-06-06 10:51:03 +02:00
Alexander Graf ed499cb49c Let --sessionfile default to a more persistent path (#659)
The presence of a sessionfile avoids the usage of the failure-prone login
mechanism.  This commit addresses a suggestion in #615 to store the sessionfile
in a persistent path rather than a path within a temporary directory if no
--sessionfile parameter is given.

The default path is now:

  $XDG_CONFIG_HOME/instaloader/session-USERNAME or
    ~/.config/instaloader/session-USERNAME on Unix,

  %LOCALAPPDATA%\Instaloader\session-USERNAME on Windows.

If no file exists in the new path, Instaloader tries loading from the path
where the sessionfile was stored before this commit, hence it automatically
migrates to the new sessionfile path.
2020-06-06 10:49:41 +02:00
Alexander Graf 1c13d5a30b Windows EXE: Start Powershell instead of cmd
... if started by file explorer.
2020-06-06 10:37:20 +02:00
Alexander Graf 2fd1422619 Windows EXE: Use psutil for startup terminal detection 2020-06-05 20:12:23 +00:00
Alexander Graf c818165db4 Update 615_import_firefox_session.py
This fixes finding the cookies.sqlite file on MacOS as per https://github.com/instaloader/instaloader/issues/92#issuecomment-638361344.
2020-06-03 20:07:52 +02:00
Alexander Graf 26a78463f1 doc: Discourage using --commit-mode
--commit-mode has many bugs, especially #257, #483.  For now we discourage using
it until it is reimplemented.
2020-06-03 18:03:27 +02:00
Alexander Graf 689dd3613b doc: Minor style-related changes to code examples 2020-06-03 18:03:01 +02:00
Alexander Graf 27b7c98be2 Update doc section Troubleshooting -> Login Error
New Firefox cookie import script, based on a comment in #615:
https://github.com/instaloader/instaloader/issues/615#issuecomment-630127064
2020-06-03 17:14:08 +02:00
Alexander Graf 72d86533f2 Downgrade 'old' stale action to more stable v1 2020-05-26 16:03:52 +02:00
Alexander Graf 7bb08a220a Update issue stale action 2020-05-24 11:25:37 +02:00
Alexander Graf 03aabc52bb Release of Version 4.4.3 2020-05-22 18:32:05 +02:00
fireattack 9d4df0d5fb Only substitute characters from auto generated fields (#631)
Only substitute characters from auto generated fields in _PostPathFormatter

This fixes issues of path problem on Windows such as #304, #529, etc.

Also made the following changes:

1. Changes substitution of colon to full-width colon (U+FF1A) instead of MODIFIER LETTER COLON (U+A789) to be consistent with others (we use full-width backslash, pipe, etc. already).
2. Also replaces `\r` on Windows with space, just like `\n`.
2020-05-22 18:30:55 +02:00
Alexander Graf bf2f4f16ed Release of Version 4.4.2 2020-05-19 09:27:03 +02:00
InfinitayandAlexander Graf 1a3712d112 Workaround for Instagram's encrypted login. (#623)
* Workaround for Instagram's encrypted login.

Credit: https://github.com/pgrimaud/
See: https://github.com/pgrimaud/instagram-user-feed/commit/96ad4cf54d1ad331b337f325c73e664999a6d066

Closes #615.

Co-authored-by: Alexander Graf <17130992+aandergr@users.noreply.github.com>
2020-05-19 09:25:08 +02:00
Alexander Graf 65462b01ec Release of Version 4.4.1 2020-05-14 16:22:42 +02:00
Alexander Graf 9857c5fa24 Regex target matching: Fix hashtag regex
Unicode characters are allowed in hashtags, so instead of just
[A-Za-z0-9], we now match all \w.  Fixes #614.
2020-05-14 16:17:07 +02:00
Alexander Graf 83f013f6ca Release of Version 4.4 2020-05-11 20:25:43 +02:00
Alexander Graf e7437310ea Add versionadded doc annotation for is_business_account and business_category_name 2020-05-11 20:24:04 +02:00
yolo dcba25a40e [Issue #601] Add is_business_account and business_category_name properties to Profile structure 2020-05-06 08:45:31 +02:00
Alexander Graf 5e4b5fd877 First Beta Release for Version 4.4 2020-04-21 10:22:03 +02:00
Lars Lindqvist 657e76e2d9 Simplify username regex
Without passing re.ASCII, \w matches non-ascii letter, such as ö, π
and я, which to my knowledge isn't allowed in usernames. Now it's
explicit which character ranges are expected in each target.
2020-04-21 10:15:24 +02:00
Lars Lindqvist 78023b3c98 Regex target matching 2020-04-21 10:15:24 +02:00
Alexander Graf ab67ca30e5 Merge branch 'master' into v4.4-dev 2020-04-21 09:53:01 +02:00
Alexander Graf 0e13c73b62 Include top posts in download_hashtag() 2020-04-19 12:21:27 +02:00
Alexander Graf b98e9941ae Omit media when saving Hashtag JSON 2020-04-19 11:30:19 +02:00
Alexander Graf c9e4f68597 Download Hashtag profilepic and save metadata JSON 2020-04-19 11:16:39 +02:00
Alexander Graf 1338cf0fcd Fix 'occured' typo in error messages
Mentioned in #573 - thanks!
2020-04-18 11:16:55 +02:00
Alexander Graf 83a0f52c60 Hashtag class 2020-04-17 18:49:38 +02:00
Alexander Graf 5c5c633bee Catch Exceptions raised during filter evaluation
Closes #192.
2020-04-10 14:39:40 +02:00
Alexander Graf e268f9e625 Release of Version 4.3.2 2020-04-10 12:08:24 +02:00
Alexander Graf 793b7e82ba Limit feed node processing to Post nodes
Fixes #478.
2020-04-10 12:05:15 +02:00
Alexander Graf 7329ee10d8 Catch KeyError: 'authenticated' when Logging In
Fixes #472.
2020-04-10 12:05:15 +02:00
Alexander Graf 57d2ce9538 Alleviate #449 by catching bad metadata response
Instead of ungracefully failing with a TypeError, the Post is now skipped with
an error message containing the invalid JSON.
2020-04-10 11:57:36 +02:00
Alexander Graf dc458a3f3c Limit feed node processing to Post nodes
Fixes #478.
2020-04-10 11:13:07 +02:00
Alexander Graf 08a359408c Improve Log Output of Skipped Posts 2020-04-10 11:12:09 +02:00
Alexander Graf a794089c4f Deduplicate Post Download Loop 2020-04-07 10:06:53 +02:00
Alexander Graf c56657855d Release of Version 4.3.1 2020-04-04 10:54:04 +02:00
Alexander Graf 8c7561c5d7 Fix Downloading of Videos within a Sidecar Post
Fixes #558.
2020-04-04 10:51:55 +02:00
Alexander Graf c76385bde2 Unit Test for Profile.get_similar_accounts() 2020-03-25 19:06:05 +01:00
Alexander Graf b5bf8a0560 Implement Profile.get_similar_accounts()
Fixes #489.
2020-03-25 19:00:03 +01:00
Alexander Graf 2790342a09 Catch KeyError: 'authenticated' when Logging In
Fixes #472.
2020-03-25 18:17:02 +01:00
Alexander Graf e725ca2640 Post properties is_sponsored and sponsor_users
This allows to access the Paid Partnership With metadata of a Post.

Closes #295.
2020-03-25 17:13:46 +01:00
Alexander Graf 9c2e7c70ac Fix number output in download_igtv() 2020-03-16 13:38:13 +01:00
Alexander Graf ffbf5b517c Release of Version 4.3 2020-03-16 09:13:25 +01:00
Alexander Graf 0843e10187 First Release Candidate for Version 4.3 2020-03-15 14:28:16 +01:00
Alexander Graf 78a93c945e Revert "Revert "Remove nonworking saythanks.io link""
saythanks.io does not work again and the link is now removed.
This reverts commit b2acbcf0e0.
2020-03-14 17:56:44 +01:00
Alexander Graf bb3e309195 First Beta Release for Version 4.3 2020-03-14 17:21:07 +01:00
Alexander Graf 6cc2ed54ea Download IGTV Videos of a Profile (--igtv) 2020-03-14 16:56:51 +01:00
Alexander Graf 41cf260fdd Fix post filter for downloading tagged posts 2020-03-14 16:55:37 +01:00
Alexander Graf f910705c51 docs: Fix formatting of request_timeout param 2020-03-07 11:14:07 +01:00
Alexander Graf aa3f19dcbe Second Alpha Release for Version 4.3 2020-03-01 17:44:17 +01:00
Alexander Graf fe258f1abb Also Download Highlight Cover Pictures
Instaloader.download_highlights(), as well as the Instaloader CLI with
--highlight option, now also downloads the cover picture of each highlight.

This closes #323, where it was suggested.
2020-03-01 17:41:58 +01:00
Alexander Graf 534497bea5 fix misplacement of --request-timeout in docs 2020-02-28 09:49:29 +01:00
André Koch-Kramer 819426efdb First Alpha Release for Version 4.3 2020-02-17 18:53:33 +01:00
Alexander Graf 0aac91680f docs: versionadded/changed directives for 4.3 2020-02-17 18:47:45 +01:00
André Koch-Kramer dc289024c8 GitHub Actions workflow for Windows EXE 2020-02-17 18:31:36 +01:00
Alexander Graf b2acbcf0e0 Revert "Remove nonworking saythanks.io link"
saythanks.io works again. This reverts commit
f966ea5bb9.
2020-02-04 09:23:02 +01:00
Alexander Graf de585becdb Rate-control non-graphql queries
Non-GraphQL queries seem to be rate limited too, and this commit adds
a safe-seeming rate-controlling for this kind of queries.

This fixes #464.
2020-02-03 17:19:49 +01:00
Alexander Graf f966ea5bb9 Remove nonworking saythanks.io link
saythanks.io stopped working, BlitzKraft/saythanks.io#60.
2020-01-27 21:31:47 +01:00
André Koch-Kramer 302145f54f Print profile suggestions when profile not found
Relates to #496.
2020-01-27 13:06:42 +01:00
André Koch-Kramer 8764947d10 Raise QueryReturnedNotFoundException if expected structure is missing
- QueryReturnedNotFoundException now inherits ConnectionException
  --> Retry on 404 errors + missing "window._sharedData"

Relates to #146 and #496.
2020-01-27 13:06:42 +01:00
André Koch-Kramer d3403bc686 Implementation of TopSearchResults class
To be able to search Instagram for profiles, hashtags and locations.
2020-01-27 13:06:42 +01:00
Alexander Graf b7bae19374 Update mypy and pylint versions for CI 2020-01-27 09:27:47 +01:00
Alexander Graf 4e87a8cf63 Fix wrong request_timeout in anonymous_copy() context
Fixes a tiny mistake introduced with #422.
2020-01-26 22:07:07 +01:00
Alexander Graf fd13d66367 Deploy documentation on tag only
Before, the documentation was pushed to GitHub Pages whenever a new commit has been pushed to master. I.e., when a new feature was added, the documentation advertised the feature although hasn’t been released yet.

Now, we only deploy the newly-build the documentation when a new tag is pushed, and ensure the the versionadded directives are added before releasing.
2020-01-26 18:54:11 +01:00
corpone2 63eb9e71ca support passing timeout to requests (#422)
Adds a request_timeout parameter to Instaloader instantiation.

Closes #330.
2020-01-13 21:59:00 +01:00
André Koch-Kramer d8ba30b91b Speed up accessing property comments of Post
This has been asked for in #436.
2020-01-13 19:56:50 +01:00
André Koch-Kramer ec0e017b10 Release of version 4.2.10 2019-10-28 12:48:27 +01:00
51bfa01c2a Solving KeyError: 'edge_sidecar_to_children' (#411)
Closes #380.
Closes #394.

Co-Authored-By: Dāvis <davispuh@users.noreply.github.com>
Co-Authored-By: Alexander Graf <mail@agraf.me>
Co-Authored-By: Thammus <koch-kramer@web.de>
2019-10-21 19:47:14 +02:00
Alexander Graf e8d4054f84 Create Stale Issue Action 2019-10-19 16:20:25 +02:00
Leonard Bogdonoff cf293ce312 Change Example Hashtag in Codesnippets 2019-10-13 19:47:44 +02:00
Alexander Graf 3226c43a98 Fix Intersphinx Mapping for Requests to fix CI 2019-10-13 19:45:14 +02:00
corpone 20819d2597 handle alternate rate limit exceeded behavior of redirecting to login 2019-08-22 19:53:52 -07:00
Alexander Graf 4e8236b6e9 Improve contributing.rst and issue templates 2019-08-20 22:05:49 +02:00
André Koch-Kramer b542936c09 Release of version 4.2.9 2019-08-18 18:28:35 +02:00
Ahmad af4dbc00d7 Added likes_count to PostCommentAnswer tuple (#379) 2019-08-18 18:18:35 +02:00
Alexander Graf 0aa1ec7c76 Getting locations now requires --login
Closes #376.
2019-08-18 10:48:46 +02:00
Alexander Graf 75a729781d 194_top_x_of_user example: Fix sort order 2019-08-10 16:25:22 +02:00
Alexander Graf a88316224b Remove ',' after URL in "Checkpoint required" message
Closes #364.
2019-08-10 16:19:35 +02:00
Alexander Graf 87dc82d1b1 Travis: Skip test with Python 3.8 beta 2019-07-27 22:04:56 +02:00
Alexander Graf 6a3b05762e Fix missing directory error in some rare cases 2019-07-27 21:08:29 +02:00
Alexander Graf 6d5bb1e181 Release of Version 4.2.8 2019-05-27 18:26:10 +02:00
Alexander Graf 819057eb6f travis: use Pipfile for dependency pinning 2019-05-27 18:15:45 +02:00
Alexander Graf 5235cc0f6a Travis: test against Python 3.8 2019-05-27 18:08:17 +02:00
Alexander Graf fb06b4b7d6 Fix iPhone query rate control
Resolves #306.
2019-05-27 18:05:51 +02:00
Alexander Graf 1a382cd61c Fixes to unittests
- Fix type issue with unittest.SkipTest

- Fix query timestamp preservation
2019-05-17 21:58:51 +02:00
André Koch-Kramer 0345da71ec Release of version 4.2.7 2019-05-17 21:44:11 +02:00
André Koch-Kramer 749ff60639 Do not require rhx_gix in metadata
Closes #297.
2019-05-17 21:38:39 +02:00
Alexander Graf 3528e8d19b Release of Version 4.2.6 2019-05-08 22:18:29 +02:00
Alexander Graf 5fce6946ea AUR deploy: Do not deploy prereleases 2019-05-08 22:10:15 +02:00
Alexander Graf 692cbc000d Use stronger pylint configuration 2019-05-08 21:49:06 +02:00
Alexander Graf 102f27c4ef doc: Add versionadded directives to new Post properties 2019-05-08 21:31:40 +02:00
Alexander Graf 33d13c4b32 Fix import issue with Python 3.5
Closes #293.
2019-05-08 21:24:00 +02:00
Alexander Graf 408cf22c18 Fix target directory with tagged and highlights
Fixes #292.
2019-05-08 21:21:16 +02:00
André Koch-Kramer 93882d93e0 Release of version 4.2.5 2019-05-07 18:04:19 +02:00
Alexander Graf 8ef7b3242c Add trendy Badges to README.rst and index.rst 2019-05-07 18:01:13 +02:00
André Koch-Kramer 406f33896a First Release Candidate for Version 4.2.5 2019-05-07 17:08:32 +02:00
André Koch-Kramer b9be1eaaf4 Automation of arch package deploy to AUR 2019-05-07 16:40:43 +02:00
Alexander Graf 6b24b54591 Post properties video_view_count and video_duration
Closes #239.
2019-05-07 15:47:52 +02:00
Alexander Graf 67d3b4fca1 Configure pylint via .pylintrc 2019-05-07 15:11:36 +02:00
Alexander Graf 4c72077976 Integrate mypy static type checker into CI 2019-05-07 13:01:16 +02:00
André Koch-Kramer 4f9d64a284 Replace non filename compatible chars in fields
Values that got inserted in dirname and filename pattern get cleared
from characters that will not work in filenames.
This is also helpful for #262.
2019-05-06 17:42:59 +02:00
André Koch-Kramer 0c9a667da7 Replace slash and backslash with similar unicode 2019-05-06 17:41:20 +02:00
Friendly ccf55dbeef New Format Specifier: {pcaption}
I saw the abbreviated caption output from save_caption and realized it was perfect for --filename-pattern.  Even if this pull request is not accepted, please do not abandon the idea within.  The ability to put captions in file names is *very* useful.
2019-04-16 19:49:31 -04:00
Dāvis 87d877e650 There can be comments without any answers 2019-04-14 00:40:46 +03:00
André Koch-Kramer adb9f3a346 Release of version 4.2.4 2019-04-01 21:46:09 +02:00
André Koch-Kramer 2aa26ea140 Disable pylint check (no-else-raise)
Freeze pylint version to 2.3.1.
2019-03-31 16:15:51 +02:00
André Koch-Kramer 0fd06fe6bc Remove needlessly complicated code 2019-03-31 15:55:25 +02:00
André Koch-Kramer 15d9cd8949 Adjust structure to handle new threaded comments
This is needed because of a structure change by Instagram.
Fixes #272.
2019-03-31 11:29:07 +02:00
Alexander Graf 9421ad5fda Release of version 4.2.3 2019-02-16 22:25:43 +01:00
Alexander Graf 1e89ebe038 Adjust GraphQL rate control 2019-02-16 19:26:58 +01:00
André Koch-Kramer 3f4c12b54b Release of version 4.2.2 2019-02-10 21:13:29 +01:00
Alexander Graf dcc57f11c4 Fix falsely-raised PostChangedException 2019-02-10 20:55:07 +01:00
Alexander Graf a9e79e90f9 Fix retry logic for download of files
This fixes #235. The retry logic has been broken with commit
4bc0a94e12 in version 4.2.1.
2019-02-10 20:29:04 +01:00
André Koch-Kramer a0b7804fd2 Properly handle redirected posts
Resolves #225.
2019-02-10 18:24:33 +01:00
André Koch-Kramer 9e04ef3436 New Profile.from_id() that works without posts
The old method needed the target profile to have at least one post.
The new method works anonymously even for private profiles.
Closes #249.
2019-02-10 17:46:18 +01:00
Alexander Graf 34416298a0 profiles: First load profilepic, then save json
This way, the iphone_struct information is included in saved profile pictures.
2019-01-24 22:40:36 +01:00
Alexander Graf e39ffecea4 Minor doc changes
- Add saythanks.io link
- Set lang="en" attribute in html tag
2019-01-24 22:39:58 +01:00
Alexander Graf 15fe3d5f2d doc: versionadded directive to new {write,get}_raw 2019-01-17 18:39:54 +01:00
Alexander Graf c73b10843f Update year in LICENSE file 2019-01-17 18:12:32 +01:00
Alexander Graf b158c57b9e Unittest for HD profilepic and fix ResourceWarning 2019-01-17 18:10:14 +01:00
Alexander Graf 57892f191a Release of version 4.2.1 2019-01-17 17:47:45 +01:00
Alexander Graf 26e74bad6a Download HD profile picture if logged in
Unfortunately, it is now required to be logged in to access the HD version of
profile pictures. When attempting to download profile pictures without --login,
a warning message is printed once, and the lower-quality versions are obtained.

For backwards compatibility, already-downloaded profile pictures are
overwritten if the now-obtainable version is assumed to be of better quality
than the existing one (determined by file size vs Content-Length).

The iPhone endpoint is accessed with code exhumed from
c355338010. Also, this reverts "Profile: don't
access removed iphone info endpoint" 08327c4117.

This fixes #209.
2019-01-17 17:39:29 +01:00
Alexander Graf 27a239f07f doc theme: Improve consistency of code highlight 2019-01-17 11:39:18 +01:00
Alexander Graf bb25f03c7d doc: "Troubleshooting" section + minor changes 2019-01-17 11:00:04 +01:00
Lars Lindqvist c54342bb98 Refactor get_filename
Use string formatting instead of `str(0) + str(index)` and don't assume file extension is three characters.
2019-01-08 19:38:46 +01:00
Alexander Graf d2bebcd00c doc: Fix +args.txt description
- Fix wrong `--option value`, which must be `--option=value`
- Fix typos

Fixes #221.
2019-01-03 17:05:43 +01:00
Alexander Graf 4bc0a94e12 Name profilepic by hash if Last-Modified missing
This fixes #188.

Also, this commit prevents double-requesting the profile pic URL to obtain the
Last-Modified header.
2019-01-03 16:49:21 +01:00
Alexander Graf 06845b53fc Fixes and enhancements to documentation theme 2019-01-01 14:23:08 +01:00
Alexander Graf e9cec0b261 docs/conf.py: remove unneeded import 2018-12-31 19:35:55 +01:00
Alexander Graf ac4cd9f595 New doc design: Sphinx Bootstrap 4 Theme
Now we use an own, mobile-first, responsive sphinx theme based on Bootstrap 4.
It makes navigation on the very long "Python module instaloader" page a lot
easier. Also, it looks better.

This solves all the problems we had with sphinx-bootstrap-theme.
2018-12-31 19:30:19 +01:00
Alexander Graf 2e7835850a Version 4.2 2018-12-23 17:07:03 +01:00
Alexander Graf 89b56bdfa8 Login: Handle suspicious login and JSON error
Motivated by #92.
2018-12-21 22:39:07 +01:00
Alexander Graf 1387d396dd Second Release Candidate for Version 4.2 2018-12-21 22:01:28 +01:00
André Koch-Kramer 4ee867c61b Implement two-factor authentication
Closes #200.
2018-12-21 21:58:12 +01:00
Alexander Graf edba6959d9 First Release Candidate for Version 4.2 2018-12-21 17:54:02 +01:00
Alexander Graf 08327c4117 Profile: don't access removed iphone info endpoint
Don't access the endpoint that was used to access the HD version of the profile
picture. This addresses #209.
2018-12-21 17:06:32 +01:00
Alexander Graf 4ab0fea82a login: print that 2FA is not supported if required
2FA support is requested in issue #200, and also mentioned in #92.
2018-12-21 16:54:56 +01:00
Alexander Graf a735b02bac doc: Add 'versionadded' directives to new features 2018-12-21 14:43:33 +01:00
Stijn Peeters be5d02ef3b Location search (#212)
* Add %location search option

Search for posts for a given location ID using %[location id] as the query

* Document %location search

* Make pylint happy

* Use correct paths for location results

* Fix —help output

Add description of location argument and fix output error for short help.

* Add unit tests for location download

* Add extra unit test for locations
2018-12-17 21:35:31 +01:00
Alexander Graf 1ab9e44104 Update sphinx_autodoc_typehints to version 1.5.2 2018-12-13 16:23:14 +01:00
André Koch-Kramer 6212d22e26 Merge branch 'master' of https://github.com/sushilicious/instaloader into v4.2-dev 2018-12-13 15:53:06 +01:00
André Koch-Kramer aa859b9f54 Ignore pylint's cyclic-import and unnecessary-pass 2018-12-13 15:29:46 +01:00
sushilicious ffda2265bd Added _committed to __init__ to make pylint shut up 2018-12-09 02:12:45 -05:00
sushilicious e753406a6c Just in case, check whether picture exists for commit
This is in addition to the other commit logic.
2018-12-09 02:12:45 -05:00
sushilicious 30555b576c Added commit mode
The commit mode ensures pictures are not corrupted when Instaloader is
unexpectedly interrupted. In the case that the last picture is corrupted
because of an interruption, Instaloader will redownload the picture.
Since the metadata is the last object saved to disk, we can consider a
post as "committed" if its json metadata file exists and is not
malformed. Instaloader should download any posts which are not
committed. Downside is commit mode requires metadata to be saved.
2018-12-09 02:12:45 -05:00
Alexander Graf ece0d11915 Release of version 4.1.1 2018-10-07 15:17:51 +02:00
Alexander Graf 41e734e1b8 Fix fast_update in case of no download
If Instaloader was called with --fast-update and encountered a post where
nothing has been downloaded because of flags that disable download of certain
types of media, the download was aborted. This behavior is now fixed.

Closes #183.
2018-10-07 14:59:16 +02:00
Alexander Graf b7ea0d5953 download_profiles: Check errors only if required
Fixes #178, where with `--profile-pic-only` an error message is printed
(without any further implications) when downloading profile pictures of private
profiles without being logged in.
2018-09-23 10:09:08 +02:00
Alexander Graf 325e89c4b2 Print how many requests were made on 429 2018-09-06 09:17:38 +02:00
Alexander Graf e248020927 Release of version 4.1 2018-09-02 21:57:20 +02:00
Alexander Graf 5a7b76db31 Merge branch 'v4.1-dev' 2018-09-02 21:56:16 +02:00
Alexander Graf 1a239e17cb First Release Candidate for Version 4.1 2018-08-31 17:42:59 +02:00
Alexander Graf bb8749b753 Adjust rate control to current rate limits 2018-08-31 17:17:23 +02:00
Alexander Graf 1f311d3e1e First Beta Release for Version 4.1 2018-08-31 12:11:43 +02:00
Alexander Graf 862c51fa81 Profile.from_id graphql query is not rate limited 2018-08-30 13:59:46 +02:00
Alexander Graf cc15cb5857 Cache and reuse profiles for Profile.from_id() 2018-08-30 13:52:16 +02:00
Alexander Graf 18707ee232 docs: Better explanation for is_video
Closes #173.
2018-08-30 10:16:56 +02:00
Alexander Graf 0e534ba519 Use username/:tagged as target for tagged posts
Subdirs in profile folders were introduced with --highlights, and moving
:tagged posts there allows to further call instaloader as

instaloader [flags] */

(#154)
2018-08-30 09:57:42 +02:00
Alexander Graf 084424e856 docs/codesnippets: Link to source tree on github 2018-08-30 09:18:58 +02:00
Alexander Graf 567a04a661 format_filename method to apply filename pattern 2018-08-30 09:13:49 +02:00
Alexander Graf 1394e8e9f5 First Alpha Release for Version 4.1 2018-08-24 12:51:52 +02:00
Alexander Graf 86204d165d Merge branch 'master' into v4.1-dev 2018-08-24 12:45:12 +02:00
Alexander Graf d0e2ac4388 docs/install: Suggest pre-release rather than git 2018-08-24 12:42:51 +02:00
Alexander Graf 60d47be2f3 Unit test for Instaloader.get_highlights() 2018-08-24 12:33:28 +02:00
Alexander Graf f988762cb1 download_profiles: raise_errors parameter
With raise_errors=True it behaves like now-deprecated download_profile().
2018-08-24 12:21:45 +02:00
Alexander Graf b443cc6654 Minor doc fixes and improvements 2018-08-24 11:53:14 +02:00
André Koch-Kramer 584c69d93c Update docs and docstrings concerning highlights 2018-08-24 00:23:19 +02:00
André Koch-Kramer cbdd85ef07 Highlights downloadable through CLI
By using --highlights all available highlight stories of target profiles
will get downloaded.
Closes #162.
2018-08-23 23:17:48 +02:00
André Koch-Kramer 2e517e972f Extend _PostPathFormatter to replace more chars
On Windows, all forbidden characters now get replaced with similar
looking unicode chars.
2018-08-23 22:19:08 +02:00
André Koch-Kramer 54572fb1fc Make the Highlight stuff accept Profile objects
download_highlights(), get_highlights() and the Highlight class now
accept and use the owner's Profile rather than creating it themselves.
2018-08-23 22:19:08 +02:00
André Koch-Kramer 158c1433bb Revert accidental change of comment.id to comment.unique_id 2018-08-23 22:19:08 +02:00
Alexander Graf 43f52198ff Fix docs building and show Highlights inheritance 2018-08-23 22:19:08 +02:00
André Koch-Kramer 91d5d5f867 Add class and functions for downloading highlights
Requested in #162.
2018-08-23 22:19:08 +02:00
Alexander Graf 0dcc912987 doc: Note new features with versionadded 2018-08-23 17:25:28 +02:00
Alexander Graf 180d398ad7 docs: Exclude sphinx-1.7.7 to fix travis build 2018-08-23 16:14:02 +02:00
Alexander Graf f262c4347f Create issue templates 2018-08-23 15:11:09 +02:00
Alexander Graf b91e73b090 set docs font size to 16px 2018-08-23 14:44:37 +02:00
Alexander Graf e388a1c966 --no-posts; Deprecate --{profile-pic,stories}-only 2018-08-20 16:29:14 +02:00
Alexander Graf 0f0ac13d72 reorder --help output 2018-08-20 14:27:58 +02:00
Alexander Graf 06b7edd6d5 --no-pictures flag to not download post pictures
Closes #131.
2018-08-20 14:27:58 +02:00
Alexander Graf 5f57345f1b "instaloader -- -SHORTCODE" to fetch single post
Closes #129.
2018-08-20 14:27:58 +02:00
Alexander Graf f7e6632f68 Document --tagged{,-only} in docs/cli-options.rst 2018-08-20 10:44:20 +02:00
AndCycle 9258b84695 Allow reading arguments from file
Squashed commit of the following (pr #161):

commit 58235a99b0
Author: AndCycle <andcycle@andcycle.idv.tw>
Date:   Thu Aug 16 17:57:26 2018 +0800

    doc: proper fix rst syntax

commit 6620e3583c
Author: AndCycle <andcycle@andcycle.idv.tw>
Date:   Thu Aug 16 17:29:25 2018 +0800

    doc: fix syntax

commit 7a048a3c04
Author: AndCycle <andcycle@andcycle.idv.tw>
Date:   Thu Aug 16 17:09:26 2018 +0800

    doc: add info about read args from file.

commit c4ceaf9636
Author: AndCycle <andcycle@andcycle.idv.tw>
Date:   Thu Aug 16 17:05:07 2018 +0800

    alter `fromfile_prefix_chars` to plus sign to avoid conflict.

commit d469b52b95
Author: AndCycle <andcycle@andcycle.idv.tw>
Date:   Mon Aug 6 19:10:23 2018 +0800

    * Allow reading arguments from file

    function referenced from instagram-scraper,
    which provide a convenient way to hide sensitive information.
2018-08-20 09:00:51 +02:00
Lars Lindqvist e00d77c234 Basic CLI support for tagged posts.
Squashed commit of the following (pr #154):

commit 8fd56c379f
Merge: 08f0ee7 a3777ca
Author: Lars Lindqvist <e5150@users.noreply.github.com>
Date:   Wed Aug 15 20:23:23 2018 +0200

    Merge branch 'master' into master

commit 08f0ee795c
Merge: 700b3a8 dcea0e9
Author: Lars Lindqvist <e5150@users.noreply.github.com>
Date:   Sun Aug 5 15:25:55 2018 +0200

    Merge branch 'master' into master

commit 700b3a8d09
Author: Lars Lindqvist <lars.lindqvist@yandex.ru>
Date:   Sat Aug 4 16:26:59 2018 +0200

    Basic CLI support for tagged posts.

commit 5e3cd10cbc
Merge: af564f5 92653dc
Author: Lars Lindqvist <e5150@users.noreply.github.com>
Date:   Fri Aug 3 19:33:24 2018 +0200

    Merge branch 'master' into master

commit af564f5174
Author: Lars Lindqvist <e5150@users.noreply.github.com>
Date:   Fri Aug 3 19:25:57 2018 +0200

    Fix owner_profile for Profile.get_tagged_posts()

commit 3cde1f7db4
Author: Lars Lindqvist <e5150@users.noreply.github.com>
Date:   Thu Jul 26 19:51:33 2018 +0200

    Add meth:get_tagged_posts to Profile
2018-08-20 08:50:29 +02:00
Alexander Graf a3777cad5d Download profile pic unless --no-profile-pic
Fixes #165.

To download (S = Stories, P = Profile Pic, p = Posts)

p	--no-profile-pic
P	--profile-pic-only
Pp	(default)
S	--no-profile-pic --stories-only
Sp	--no-profile-pic --stories
SP	--stories-only
SPp	--stories
2018-08-13 17:57:28 +02:00
André Koch-Kramer 8303f1d1c5 Release of version 4.0.8 2018-08-07 20:07:41 +02:00
Alexander Graf 998333b7cd Revert "Mention IRC channel #instaloader on Freenode"
This reverts commit 8423e3bd00. The channel did
not have as much activity as expected and an IRC channel does not work if there
are not at least a few people in it.
2018-08-07 19:57:10 +02:00
André Koch-Kramer 77ca78e977 Fix logging out when download profile_pic fails
Add try-finally-block to InstaloaderContext.anonymous_copy().
Fixes #159.
2018-08-07 19:16:08 +02:00
Alexander Graf dcea0e9672 Release of version 4.0.7 2018-08-05 09:37:18 +02:00
Alexander Graf 809b23a996 doc: Mark Profile.has_higlight_reels as deprecated 2018-08-05 09:13:49 +02:00
Alexander Graf c634f21dcc Fix anon-retry for profiles that blocked viewer
This was broken with 50a5330fec.
2018-08-05 09:01:22 +02:00
Alexander Graf a070ed42ae get_stories(): Fetch in chunks of 100 users
It seems there is a limit of how many user stories can be requested at once.
Now, Instaloader.get_stories() chunks the stories request.

Fixes #157 and fixes #134.
2018-08-05 08:06:53 +02:00
Alexander Graf a34d750152 rate handling: Show %H:%M when request is retried 2018-08-05 08:02:35 +02:00
Lars Lindqvist bfa5d8ed41 Fix :stories
50a5330fec breaks `:stories` if userids is None (non-iterable).
2018-08-05 03:29:24 +02:00
Alexander Graf 7f81985911 _ArbitraryItemFormatter: Allow chaining attributes
Override get_value() rather than get_field(), to allow chaining of item
attributes, such as --post-metadata-txt={owner_profile.profile_pic_url}.

Motivated by #151.
2018-08-04 11:41:19 +02:00
Lars Lindqvist b5aa268344 Fix owner_profile for Profile.get_tagged_posts() 2018-08-04 09:41:23 +02:00
André Koch-Kramer 92653dcdb9 Release of version 4.0.6 2018-08-02 16:55:59 +02:00
André Koch-Kramer 1b5c12c8fa Add unittest for Profile.get_tagged_posts() 2018-08-02 16:48:06 +02:00
Lars Lindqvist bc29ffbd44 Add meth:get_tagged_posts to Profile 2018-08-02 16:41:51 +02:00
André Koch-Kramer 50a5330fec Change download of stories when not using :stories
If using --stories or --stories-only the stories got donwloaded along
with the profiles one by one. Now, the stories get downloaded in a
similar aproach like when using the :stories target, i.e.
download_stories() gets only called once.
Profile.has_highlight_reels is broken and now always returns true.
This fixes #153.
2018-08-02 16:20:27 +02:00
André Koch-Kramer 1739844758 Fix login by allowing more http headers
Because Instagram is bombarding over hundret of headers on login it is
necessary to increase http.client._MAXHEADERS.
Fixes #152.
2018-07-30 17:50:22 +02:00
André Koch-Kramer 0ebb223214 Added BTC address to donations section
Motivated by #130.
2018-07-23 23:36:46 +02:00
Alexander Graf 3d4109ded3 travis disable pylint's no-else-return 2018-07-22 11:04:11 +02:00
Alexander Graf 8423e3bd00 Mention IRC channel #instaloader on Freenode 2018-07-22 10:55:29 +02:00
Alexander Graf 5b0f8b75cc Add AUTHORS.md listing Instaloader's authors 2018-07-22 10:54:54 +02:00
Alexander Graf 3cb33603e5 doc: Use Instaloader brand colors for links 2018-07-09 21:32:18 +02:00
Alexander Graf 47db7f7ffa Release of version 4.0.5 2018-07-09 10:18:27 +02:00
Alexander Graf 2567bec975 unittests: Test order of Profile.get_posts()
Motivated by #140.
2018-07-09 10:08:18 +02:00
Alexander Graf 2df1419d68 Fix file extension of profile picture
Fix #142.
2018-07-09 10:07:16 +02:00
Alexander Graf 8211c9daf1 Documentation Improvements
- Fix Spelling
- codesnippets: Improve introduction texts; Write subsection about
  metadata JSON files
2018-06-17 18:10:19 +02:00
Alexander Graf 969d4e3aa4 Post: Write all ever-obtained metadata into JSON
The Post._asdict() code assumed that Post._node always is a subset of
Post._full_metadata_dict. However, it turned out, that `edge_liked_by`,
`thumbnail_src` and `thumbnail_resources` may be contained in Post._node, but
not in Post._full_metadata_dict. Now the keys of both dictionaries are saved in
the JSON file.

Fixes #137.
2018-06-15 17:07:28 +02:00
Alexander Graf 4cf1ae847d Mention donation option in doc
(Suggested by #130)
2018-06-13 08:39:29 +02:00
André Koch-Kramer a276a0dc59 Release of version 4.0.4 2018-06-07 15:46:47 +02:00
André Koch-Kramer cc34b8340f Fix detection of sidecars and videos
This was noticeable when downloading hashtags.
Fixes #133
2018-06-07 15:32:23 +02:00
Alexander Graf 45fe509ac4 doc: Link codesnippets in basic-usage 2018-06-04 12:06:41 +02:00
Alexander Graf ae492ed68b New doc section: codesnippets / Advanced Examples
Presents code examples that use the instaloader module for more advanced tasks
than what is possible with the Instaloader command line interface.

Presents #46, #56, #110, #113, #120, #121.
2018-06-04 11:57:28 +02:00
Alexander Graf 0e433294ad unittests: Preserve query timestamps between tests
This avoids getting rate limited while running all the tests.
2018-05-30 13:16:16 +02:00
Alexander Graf a807408d8f Release of version 4.0.3 2018-05-23 08:57:36 +02:00
Alexander Graf 86035b8d2d Fix obtaining csrftoken for login
Fixes #122. Further fixes processing of login JSON response regarding error
handling.
2018-05-23 08:50:37 +02:00
Alexander Graf a3eb93c21a Profile: Cache iphone data structure if queried
Profile.get_profile_pic_url() and Profile.has_highlight_reels did the same
query without remembering the response, resulting in that query being made
twice when downloading stories and profile pic of a profile.
2018-05-19 09:52:07 +02:00
André Koch-Kramer b9c7713d6a Release of version 4.0.2 2018-05-14 12:47:20 +02:00
Alexander Graf 60d35a95df docs: note that --stories-only @login is bad
Motivated by #112, #118.
2018-05-14 11:04:48 +02:00
Alexander Graf 920dcd0407 Have Profile.{followers,followees,external_url} 2018-05-14 10:46:04 +02:00
Alexander Graf d19dd2c9d4 Fix internal references in docs/as-module
Further, document all attributes that are referred to, and ensure that there
will be no broken references again by letting Travis call Sphinx with -W -n.
2018-05-14 10:46:04 +02:00
Alexander Graf be43ebb5c5 Unittest for Profile.has_viewable_story 2018-05-14 10:42:31 +02:00
Alexander Graf 52eb6d8e27 Fix minor issues regarding last commit
- Close session from InstaloaderContext.anonymous_copy().

- Have graphql_count_per_slidingwindow default value in one place rather than
  three.
2018-05-14 10:27:34 +02:00
André Koch-Kramer 3211d63ec1 Replace deleted field has_highlight_reel
Since the property has_highlight_reel is no longer available throught
the previously used graphql query, this information needs to be obtained
in another way. Therefore the properties has_highlight_reels,
has_public_story and has_viewable_story were added to the Profile class.
Since has_public_story can be obtained throught graphql queries without
being rate limited when invoked anonymously, the ability to use an
anonymous copy of the context was added to to the InstaloaderContext class.
Fixes #116
2018-05-13 19:43:49 +02:00
André Koch-Kramer e1228501d2 Set graphql_count_per_slidingwindow to 20
Instagram tightened their rate limits again.
Implemented hidden switch --graphql-rate-limit for easier adjustment of
graphql_count_per_slidingwindow.
Fixes #118
2018-05-13 19:09:40 +02:00
Alexander Graf b8797cf337 Release of version 4.0.1 2018-05-08 19:16:55 +02:00
Alexander Graf fa06e90e09 Fix anonymous retry logic 2018-05-08 19:12:42 +02:00
Alexander Graf d464396d6e Adjust rate control behavior
Increment graphql_count_per_slidingwindow to 25; Let context._sleep() choose
sleep times from upper-bounded exponential distribution with similar expected
value as before, letting wait times between queries seem more natural.
2018-05-08 18:54:28 +02:00
Alexander Graf 9961312d34 Fix anonymous retry on profile that blocked viewer 2018-05-08 18:15:17 +02:00
Alexander Graf 211b2269ff download_profile: Request story only if has_highlight_reel
This is a simple modification to save GraphQL queries, hence to more likely
stay within rate limits without having to wait between profiles.

Fixes #112.
2018-05-08 18:10:28 +02:00
Alexander Graf b3df5a5f07 Fix bugs in Instaloader.update_comments()
- Cast id and created_at from JSON to int to be compatible with comments JSON
  files from Instaloader 3.3,
- Do not fail if comment JSON files are empty,
- Close file descriptors of comment JSON files when reading,
- Do no create empty comment JSON files if generating content fails.

Closes #114.
2018-05-08 17:39:00 +02:00
Alexander Graf 5204990db9 Release of version 4.0 2018-05-02 17:38:07 +02:00
Alexander Graf 94df221da2 Merge branch 'v4-dev' 2018-05-02 17:29:12 +02:00
Alexander Graf e7decba5ee main: Enumerate profiles separated with ' ' in log 2018-05-02 17:27:46 +02:00
Alexander Graf a3a09e1952 Very minor tweak to a paragraph in basic-usage.rst 2018-05-02 17:26:53 +02:00
Alexander Graf b1e55a41a4 Do not log Post's username in download_saved_posts
This improves speed, as get_saved_posts() returns Posts that do not have
owner_profile info. So, printing the owner_username often required an
additional request.
2018-05-01 20:22:52 +02:00
Alexander Graf b5ad92e236 Trivial improvement to Profile.from_id() 2018-05-01 19:52:39 +02:00
Alexander Graf 084cfb102c Handle ValueError on bad profile id file
Fixes "ValueError: invalid literal for int() with base 10: b''", reported in #95.
2018-05-01 15:14:19 +02:00
Alexander Graf 2722da6ae4 Fix anon username from id (find renamed profile)
maybe related to #95, #104, 67ac8f3397.
2018-05-01 13:11:40 +02:00
Alexander Graf 1469064176 Minor documentation tweaks
- Instaloader as Cronjob sections

- Many new internal links to ease navigation
2018-04-30 09:56:43 +02:00
Alexander Graf 18ef484f3a First Release Candidate for Version 4.0 2018-04-29 22:53:20 +02:00
Alexander Graf dba74ab8d4 Minor documentation improvements 2018-04-29 16:41:24 +02:00
Alexander Graf dd80ef484c Improve formatting of basic-usage/what-to-download 2018-04-29 16:26:31 +02:00
Alexander Graf 6117e223a7 Describe Metadata Text Files Feature 2018-04-29 16:24:55 +02:00
Alexander Graf 7c76202f1c Update documentation regarding v4 changes
as-module.rst: List how Post and Profile is created

Describe new filename specification logic

Update filter-posts section
2018-04-29 13:33:25 +02:00
Alexander Graf 5036cc7b44 main: Ignore profile JSON files as target
It might annoy or be undesired when using Instaloader with a globbed
JSON file list, i.e. instaloader profile/*.json to just update all post
metadata.
2018-04-29 11:22:56 +02:00
Alexander Graf 8a8ea2913c Improve post/storyitem-metadata-txt behavior
If an expression evaluates to None, an empty string is yielded, rather
than 'None'.

Blanks (and newlines) are strip()ed from the formatted strings, which
also avoids creation of whitespace-only textfiles.

save_captions parameter to Instaloader constructor has been removed, as
it is equivalent to setting both {post,storyitem}_metadata_txt_pattern
to '' (empty string).

Instaloader.save_caption() now prints the '[{old}] updated [{new}]' if
the caption was updated, rather than printing the new caption twice,
which was confusing.
2018-04-29 11:17:48 +02:00
Alexander Graf b2dd395a86 Link cli-options in Instaloader __init__ docstring 2018-04-29 10:13:31 +02:00
Alexander Graf d21fb4154e fine-tune as-module.rst 2018-04-28 21:45:57 +02:00
Alexander Graf 7edc69454d doc: Fix links to Instaloader classes 2018-04-28 19:50:14 +02:00
Alexander Graf 147cd1a580 Post_get_comments() yield namedtuple PostComment 2018-04-28 18:17:00 +02:00
Alexander Graf 0e40da1c70 Let Post.get_likes() yield Profile instances 2018-04-28 17:32:13 +02:00
Alexander Graf 4dcb23cfe5 {Post,Profile,StoryItem}.get_node() -> ._as_dict()
This is consistend with namedtuple._as_dict() and marks it as a
protected member, which is good as it should be only accessed by
save_structure_to_file().
2018-04-28 17:15:42 +02:00
Alexander Graf 3dcfcff04c namedtuples PostLocation and PostSidecarNode
Post.get_sidecar_edges() has been renamed to Post.get_sidecar_nodes().

Post.get_location() has been made to a property Post.location and it now
internally stores location struct and makes it JSON store-/loadable.
2018-04-28 17:11:08 +02:00
Alexander Graf 2c50972e08 Update as-module.rst and docstrings 2018-04-28 15:47:04 +02:00
Alexander Graf b9ac40899d Theme-related Documentation Enhancements
- Consistent background color in all types of codes
- Improve dd (list item) padding
- Link to Index in global TOC in navbar
- Briefly describe Instaloader in `index.html` title
- Remove page contents (navbartoc) in index, genindex and installation
- Link to new pypi.org rather than old pypi.python.org
2018-04-28 15:47:04 +02:00
André Koch-Kramer e25eb2a948 Limit GraphQl queries to 20 per 11 minutes
cherry-picked from commit d90c05e0a4
- Set GRAPHQL_PAGE_LENGTH to 50 what appears to be the new working maximum.
- Limit GQL queries to 20 per 666 seconds.
- Remove logic for tracking queries per query identifier as Instagram
  only allows 20 overall GQL queries per sliding window.

Related to #101
2018-04-26 17:13:59 +02:00
André Koch-Kramer 440c59ecb9 Release of version 3.3.10 2018-04-20 18:51:55 +02:00
André Koch-Kramer c9198d232f Add 'disable=protected-access' to satisfy pylint 2018-04-20 18:47:36 +02:00
André Koch-Kramer e689dadf00 Output warning when fetching special data
When fetching comments, likes, followees or followers, output a warning
that the used queries are rate limited.
2018-04-20 15:34:43 +02:00
André Koch-Kramer d90c05e0a4 Limit GraphQl queries to 20 per 11 minutes
- Set GRAPHQL_PAGE_LENGTH to 50 what appears to be the new working maximum.
- Limit GQL queries to 20 per 666 seconds.
- Remove logic for tracking queries per query identifier as Instagram
  only allows 20 overall GQL queries per sliding window.

Related to #101
2018-04-20 12:59:41 +02:00
Alexander Graf 57ce51d4ef Support datetime objects in filter strings
Now --only-if="date_utc<datetime(2018,1,1,hour=12)" is possible.
2018-04-19 15:42:26 +02:00
Alexander Graf 7d2830b8b2 Have --storyitem-filter (--only-if for StoryItems) 2018-04-19 15:12:05 +02:00
Alexander Graf 7688bdce45 Reverse story download order to enable fast-update
Port of 729aa31d86 to v4-dev branch.
2018-04-18 16:38:13 +02:00
Alexander Graf eecaca7c7b Release of version 3.3.9 2018-04-18 16:25:53 +02:00
Alexander Graf 729aa31d86 Reverse story download order to enable fast-update
Fixes #100.
2018-04-18 16:24:47 +02:00
Alexander Graf 94df60292e Fix bug about 429 handling
It caused Instaloader to fail with a TypeError on 429 Too Many Requests.
2018-04-18 16:24:20 +02:00
Alexander Graf 2d9acd9989 Slightly improve error handling
Use raise from rather than raise where appropriate

Have a logic to reduce graphql page length if 400 Bad Request is
returned
2018-04-18 15:58:45 +02:00
Alexander Graf 09f2eb3601 Set GRAPHQL_PAGE_LENGTH to 12
This is the length the browser requests. Might alleviate #94.
2018-04-18 11:41:00 +02:00
Alexander Graf 066c3de113 get_followe{e,r}s yield Profiles rather than dicts 2018-04-17 15:11:50 +02:00
Alexander Graf 9d8175354b Add paging tests for profiles and stories 2018-04-17 14:23:12 +02:00
Alexander Graf dcd16f29c2 Split tests TestInstaloader{Anonymously,LoggedIn} 2018-04-17 14:10:27 +02:00
Alexander Graf 5d513da73c Fix x-instagram-gis calculation
Port of f16b99bec0 to v4-dev branch.
2018-04-17 14:01:11 +02:00
Alexander Graf 3e904e8a14 Release of version 3.3.8 2018-04-17 13:30:45 +02:00
Alexander Graf f16b99bec0 Fix GraphQL Queries (again)
Fixes #96.
2018-04-17 13:17:50 +02:00
Alexander Graf 59152214cf Allow specification of what to write in txt files 2018-04-13 22:54:40 +02:00
Alexander Graf 80f701150c New formatter for filename pattern 2018-04-13 21:19:04 +02:00
Alexander Graf df1cdb5d48 Remove now-unneeded Tristate 2018-04-13 19:07:23 +02:00
Alexander Graf 7976a4811e Fix x-instagram-gis calulation
Port of efa0b93fda to v4-dev branch.
2018-04-13 18:34:35 +02:00
Alexander Graf d316ddf02a Release of version 3.3.7 2018-04-13 18:17:40 +02:00
Alexander Graf efa0b93fda Fix rhx_gis calculation to fix 403 (fix #94) 2018-04-13 18:05:36 +02:00
Alexander Graf 8ab7d3398f Rename Instaloader.download_story{,item}
It makes more sense since the object is called StoryItem.
2018-04-12 22:04:06 +02:00
Alexander Graf f0bebd0d96 Saving and reimporting of JSON files
Metadata JSON files are now created by default. They can later be given
as a target to redownload Posts, StoryItems or Profiles with new
settings.
2018-04-12 22:03:02 +02:00
Alexander Graf 5d249c5401 Fix bug concerning Post class 2018-04-12 17:18:53 +02:00
André Koch-Kramer bd9e9d4a06 Fix bug concerning Story class 2018-04-12 16:57:46 +02:00
Alexander Graf 9b3014d5bf Fix anonymous GraphQL queries
Port of 73ec884ea4 to v4-dev.
2018-04-11 21:24:36 +02:00
Alexander Graf 020830d591 Allow graphql_node_list() to take a first page 2018-04-11 20:44:17 +02:00
André Koch-Kramer ced65ea401 Release of version 3.3.6 2018-04-11 14:32:42 +02:00
André Koch-Kramer 73ec884ea4 Fix anonymous GraphQL queries
Closes #94.
2018-04-11 14:19:24 +02:00
Alexander Graf 74d5e35eb8 get_{comments,likes}: save requests
Post.get_{comments,likes} had a logic to save requests if comments/like
count is low. That logic had a bug that is now fixed.

Related to #94.
2018-04-11 11:24:49 +02:00
Alexander Graf 25b8165547 Various minor fixes
* Fix Post.owner_profile in cases where initial owner struct contains id
  only

* Assert node structure is complete enough at Post creation

* Fix get_followees() call in _main()

* Prefer importing from '.' rather than '.<submodule>' in __main__

* Fix name of instaloader_unittests ('-' is illegal in module names)

* Foreport 5fe2a70374
  "Fix downloading of hashtags"

* Foreport 67ac8f3397
  "Enforce being logged in for getting username by id"
2018-04-11 10:49:03 +02:00
Alexander Graf 8612712f44 Release of version 3.3.5 2018-04-10 20:57:53 +02:00
Alexander Graf 67ac8f3397 Enforce being logged in for getting username by id
It seems it is not possible at the moment to get username by userid
anonymously.
2018-04-10 20:52:39 +02:00
Alexander Graf 5fe2a70374 Fix downloading of hashtags 2018-04-10 20:34:07 +02:00
André Koch-Kramer 5b9590a768 Story and StoryItem classes to represent stories 2018-04-10 20:29:20 +02:00
Alexander Graf d90d67d619 Refactor Profile to allow lazy initialization
This changes also allows us to use Profile class in more situations,
which makes the code better.
2018-04-10 15:25:36 +02:00
André Koch-Kramer dd1cbf0a1d Fix instaloadercontext.get_and_write_raw() 2018-04-10 09:51:35 +02:00
Alexander Graf 8bd9c4449c Theme-related Documentation Enhancements
- Consistent background color in all types of codes
- Improve dd (list item) padding
- Link to Index in global TOC in navbar
- Briefly describe Instaloader in `index.html` title
- Remove page contents (navbartoc) in index, genindex and installation
- Link to new pypi.org rather than old pypi.python.org
2018-04-09 16:07:08 +02:00
Alexander Graf c7f6cff15a basename(argv[0]) instead of argv[0] in usage str 2018-04-06 17:40:33 +02:00
Alexander Graf c71be83f43 Split module into multiple files 2018-04-06 17:40:22 +02:00
Alexander Graf 3511811090 Move Instaloader.main method to module-level _main 2018-04-06 17:40:22 +02:00
Alexander Graf c350847b50 InstaloaderContext class for log and low-level IO 2018-04-06 17:40:13 +02:00
Alexander Graf 8623829696 Strip trailing '/' chars from target specifiers
This makes Instaloader more shell-autocompletion-friendly.
2018-03-31 15:18:05 +02:00
Alexander Graf 235522dfd7 Merge branch 'master' into v4-dev 2018-03-31 15:06:39 +02:00
André Koch-Kramer edd1fbe49c Release of version 3.3.4 2018-03-29 20:36:53 +02:00
André Koch-Kramer dae18a05c6 Use anonymous session for profile pic query 2018-03-29 20:33:20 +02:00
André Koch-Kramer c5b60b8a12 GraphQL for post locations 2018-03-29 20:31:39 +02:00
André Koch-Kramer 40a87761c9 Mention Windows standalone exe in documentation 2018-03-29 19:47:35 +02:00
André Koch-Kramer d9b5c77e3a Implement high quality profile pic hack
Use 'i.instagram.com' API to retrieve the best quality version of the
profile pic.
Related to #85.
2018-03-29 19:33:05 +02:00
Alexander Graf 5b6e8f1a31 Fix indent in USAGE_STRING message 2018-03-29 11:19:15 +02:00
Alexander Graf d533c60824 doc: Switch to mobile-friendlier bootstrap theme
It seems to be better supported than Alabaster and is easier to be
configured to look good on both a phone and a desktop.
2018-03-28 19:39:58 +02:00
Alexander Graf 539273b963 make doc more mobile-friendly 2018-03-24 21:47:35 +01:00
André Koch-Kramer 71331d37e4 Removed modifying of profilepic url
Since Instagram does not allow anymore to download pics without a valid
signature given in the URL, Instaloader has to be content with a
resolution for the profile pic that is provided in the JSON data.
Closes #85.
2018-03-24 19:54:07 +01:00
Alexander Graf 4153bf27a9 Disable pylint invalid-name message in travis 2018-03-24 19:44:31 +01:00
Alexander Graf 938e029592 Always close sockets and files 2018-03-24 19:40:29 +01:00
Alexander Graf d3bc7cb82b Add unit tests invoking all server requests 2018-03-24 19:38:29 +01:00
Alexander Graf 36110c8727 Disable search engine indexing on old doc hoster 2018-03-24 12:37:24 +01:00
Alexander Graf 4a944fbbdb doc: meta tag to redirect to new documentation URL 2018-03-24 11:32:53 +01:00
André Koch-Kramer 91976e595a Implements get_explore_posts() 2018-03-22 17:36:03 +01:00
Alexander Graf a492f0a6d1 Merge branch 'master' into v4-dev 2018-03-22 16:10:35 +01:00
Alexander Graf 6764cd961f Tweak documentation description html meta tags 2018-03-22 16:07:19 +01:00
Alexander Graf a125b367e3 Release of version 3.3.3 2018-03-22 14:56:29 +01:00
Alexander Graf 07b30d6ebd Log message if Instaloader is called w/out target 2018-03-22 13:42:08 +01:00
Alexander Graf 3640e9c056 Replace : by \ua789 (꞉) in pathnames on Windows
Workaround for #84.
2018-03-22 11:25:20 +01:00
André Koch-Kramer 90a1b73d21 Fix format_string_contains_key()
AttributeError occured if no parsable field was found in the specified
pattern.
Reported in #84.
2018-03-21 22:25:40 +01:00
André Koch-Kramer b3bae50ca1 Consistently replace all GQL Query IDs by Hashs 2018-03-19 21:20:19 +01:00
André Koch-Kramer 2b9ed927c7 Proper handle HTTP redirects in get_json()
This is necessary to preserve the GET parameters across redirects.
Additionally, it is nice to have a log output if a redirect occurs.
2018-03-19 21:05:13 +01:00
Alexander Graf 706b4cf3e6 Support @profile again to download followees
(it got lost in one of the last commits)
2018-03-18 20:41:39 +01:00
André Koch-Kramer c1b9aa5fba Download stories using GraphQL queries
As suggested in #71, stories are now fetched using GraphQL queries.
This obviates the need of pretending to be a smartphone app and to do
some voodoo to the headers.
2018-03-18 20:17:47 +01:00
Alexander Graf 77a44362aa @_requires_login auto-add :raises: docstring entry 2018-03-18 20:17:19 +01:00
Alexander Graf a9fb254d76 "@_requires_login" for logged-in-only methods
Methods in Instaloader class can now be prefixed with @_requires_login
decorator to ensure they are only called if logged in, otherwise
LoginRequiredException is raised.
2018-03-18 20:04:35 +01:00
Alexander Graf b0a8bfbcf4 Merge branch 'master' into v4-dev 2018-03-18 18:13:46 +01:00
André Koch-Kramer a7f89e4327 Release of version 3.3.2 2018-03-18 18:10:18 +01:00
André Koch-Kramer af1ec22d41 Fix get_feed_posts() using graphql queries
Due to changes at Instagram's API, it seems like it is no longer
possible to get users' feeds without usage of graphql queries on some
accounts.
2018-03-18 17:56:33 +01:00
André Koch-Kramer f55334cd5b Fix KeyError on login attempt with wrong password 2018-03-18 17:41:03 +01:00
Alexander Graf 4a2e32790a Minor documentation improvements
- List all __init__ methods in documentation
- Further describe purpose of Profile in its docstring
2018-03-18 17:19:22 +01:00
Alexander Graf 760688d9e6 Profile class abstracting profile structure 2018-03-18 16:31:32 +01:00
Alexander Graf 279de0a641 List all Exceptions in doc, fix name inconsistcies 2018-03-18 16:29:34 +01:00
André Koch-Kramer b2f31e7782 Add awesome logo to README
Due to rst limitations, the heading "Instaloader" was added beside the
logo inside the png.
2018-03-14 23:08:04 +01:00
Alexander Graf 68d8f65cb9 Add logo in documentation sidebar 2018-03-14 22:14:24 +01:00
Alexander Graf e97e4ecdc8 Further enhance logo 2018-03-14 19:41:20 +01:00
Alexander Graf e77c998a94 Invent beautiful logo and use it as doc favicon 2018-03-14 19:09:24 +01:00
Alexander Graf 861d7b92f1 Update Year in LICENSE file 2018-03-14 11:42:23 +01:00
Alexander Graf bfc882306b Release of version 3.3.1 2018-03-14 11:37:43 +01:00
Alexander Graf 14e9cf435e Fix download of saved posts
(was broken with commit 83b31175df, due to
a slight modification of Instaloader.get_profile_metadata)
2018-03-14 08:41:48 +01:00
Alexander Graf 83b31175df Support new GraphQL profile metadata structures
Fixes downloading of profiles, reported in #81.
2018-03-14 08:18:54 +01:00
Alexander Graf 92114f9018 Add message on instaloader.rtfd.io that doc moved 2018-03-13 16:58:00 +01:00
Alexander Graf 58a80c349d Release of version 3.3 2018-03-13 16:34:05 +01:00
Alexander Graf 5f34fca9e7 Merge branch 'v3.3-dev' 2018-03-13 16:22:37 +01:00
Alexander Graf fd6389249c Describe return type of Post.get_{likes,comments} 2018-03-13 16:19:26 +01:00
Alexander Graf 374f13adbf Introduce Instaloader.GRAPHQL_PAGE_LENGTH 2018-03-13 16:10:38 +01:00
Alexander Graf 17e443a7e2 Post.get_likes() to iterate over likes of Post
Wished in issue #70.
2018-03-13 15:57:12 +01:00
André Koch-Kramer 4628f1cc8d Reimplemented test_login() via graphql query
Fixes # 76.
2018-03-13 15:37:35 +01:00
Alexander Graf e97411a502 Mention :saved in all places of documentation 2018-03-13 15:36:27 +01:00
Alexander Graf 304cde64fe Link to github pages doc hosting rather than rtd 2018-03-13 15:11:36 +01:00
Alexander Graf 3dbce922c3 travis: remove checks on python 3.7 and nightly
These failed because of Sphinx.
2018-03-13 14:11:55 +01:00
Alexander Graf 4e57ac77f0 docs: Obtain current tag via git instead of gh api 2018-03-13 14:03:49 +01:00
André Koch-Kramer de960b2b57 Mentione target :saved in README, docs and --help 2018-03-13 13:53:21 +01:00
Alexander Graf 9b60adeb45 Adjust github repo references
New repo URL: https://github.com/instaloader/instaloader
2018-03-13 13:29:44 +01:00
Alexander Graf 0333bb5e99 Travis to push doc to instaloader.github.io 2018-03-13 13:22:10 +01:00
André Koch-Kramer b1edaddb31 Support for new target :saved
If logged in, Instaloader is now able to download posts which are marked
as saved. This feature was suggested in #78.
2018-03-13 12:02:45 +01:00
André Koch-Kramer 01c2a2b1cb Release of version 3.2.2 2018-02-15 16:13:36 +01:00
André Koch-Kramer 541b29b2e9 Remove signatures from profile pic URLs
Fixes #75 as suggested by @e5150.
2018-02-15 16:10:04 +01:00
André Koch-Kramer 6f3afd2fa1 Removed redundant character escapes in RegExp 2018-02-15 15:01:17 +01:00
André Koch-Kramer 882d460a67 Merge branch 'bug_69_fix' 2018-02-15 14:56:42 +01:00
André Koch-Kramer 9b11ca18d1 Added backward compatibility for --fast-update
This is related to bug #69.
Filenames containing the old date encoding without UTC are now
recognized when using the `--fast-update` switch.
Also non-picture files using the old date encoding should now be
recognized even when not using the `--fast-update` switch, excluding
geotags and the post metadata JSON files. The files containing comments
get renamed to the new format when touched. Caption files are only
renamed, if they get updated. Stored picture files do not get renamed at
all.
2018-01-28 16:40:13 +01:00
Alexander Graf 39bd7c55f6 doc: link next section at the end of each page
This makes it easier to go through the documentation and it
pushes readthedocs's advertising further apart from the
content.
2018-01-13 19:15:32 +01:00
André Koch-Kramer 3654d6bf6d Use UTC in filenames per default
This adresses bug #69.
All fields in the filename pattern that should be replaced by a date
encoding string (excluding {post.date_local}) are now evaluated using
UTC while ensuring that the replacing string indicates the use of UTC.
2018-01-10 22:26:49 +01:00
Alexander Graf 1ca368fa45 Mention Arch+Gentoo pkgs in doc/install section 2018-01-07 22:08:51 +01:00
Alexander Graf 698ea51cb9 Add contribution guidelines to documentation 2018-01-07 17:26:35 +01:00
Alexander Graf e6be0179b7 Support {date_utc} filename_pattern
{date_utc} encodes the post creation date in UTC rather than the
current local timezone, as {date} does.

This was proposed in #69. Encoding the post creation date in
local time zone induces problems regarding --fast-update when the
time zone is changed.
2018-01-07 14:52:03 +01:00
Alexander Graf 97e1bd2bf0 Disable Pylint's new inconsistent-return-statements 2017-12-21 14:03:44 +01:00
Alexander Graf 5695bca9f3 Current version and release date in doc sidebar 2017-12-21 13:53:06 +01:00
Alexander Graf 75d9dda4ba --no-video-thumbnails option
Closes #61.
2017-12-14 19:16:39 +01:00
Alexander Graf 41a50ffa5a Refactor download_story() into a method 2017-12-14 16:17:46 +01:00
Alexander Graf 3daaa6fbbc --no-profile-pic to disable profile pic download
(Suggested in #61)
2017-12-14 15:55:53 +01:00
André Koch-Kramer 341030058b Release of version 3.2.1 2017-12-11 21:21:07 +01:00
André Koch-Kramer fd98e8e78f Reduced GraphQL query chunks to 200
Fixes #59.
2017-12-11 21:11:18 +01:00
Alexander Graf e24c6a2cad Add description meta tags to documentation 2017-12-03 15:20:51 +01:00
Alexander Graf 01e549275e doc: ownership verification tag 2017-11-17 10:06:53 +01:00
Alexander Graf 37780986fc doc: Page intro improvements
The introductionary sections of as-module and cli-options have been
rewritten. Further, Instaloader's short-description is now included in
the sidebar.
2017-11-14 11:18:30 +01:00
André Koch-Kramer 9f34e3303e Added basic usage to README and documentation 2017-11-10 18:55:52 +01:00
Alexander Graf d5a8d96bd5 Release of version 3.2 2017-11-09 18:37:25 +01:00
André Koch-Kramer e9207f095f Use Post class attributes in filename-pattern
- Added owner_id and mediaid to Post class properties.
- In case of not downloading stories, the attributes of the Post class
  can now be used in filename-pattern, e.g. {post.owner_id} or {post.mediaid}.

Closes #53.
2017-11-08 15:58:33 +01:00
André Koch-Kramer ce13c0c53c Corrected file extension on videos from stories
Closes #54.
2017-11-08 13:45:29 +01:00
Alexander Graf 7d5bf0e8df Fix Documentation link in docs/README.md 2017-11-03 10:59:52 +01:00
Alexander Graf bdf31864eb Create docs/README.md 2017-11-03 10:46:50 +01:00
Alexander Graf 32388b884f Fix typos in documentation and --help output 2017-10-30 18:38:03 +01:00
Alexander Graf 7e941f5a81 Release of version 3.1 2017-10-01 12:09:37 +02:00
Alexander Graf a1920b97a4 More control about connection retry mechanism
- If when retrying a failed connection ^C is hit, the retrying is
  aborted,

- Option --max-connection-attempts to override default of how many
  connection attempts should be made.

Closes #51.
2017-09-29 16:09:15 +02:00
Alexander Graf e0ed4cf16c Save metadata JSON with --metadata-json
With --metadata-json, a JSON file for each post is created saving the
Post properties defined in instaloader.Post class, i.e. caption, number
of likes, people tagged in caption or the picture itself, etc.

This closes #33 and closes #47.
2017-09-29 14:07:00 +02:00
Alexander Graf e471bd5ad3 Post properties caption_mentions and tagged_users
caption_mentions is a list of all lowercased profiles that are mentioned
in the Post's caption, without preceeding '@'.

tagged_users is a list of all lowercased users that are tagged in the
Post. This was requested in #47.

Just like all properties of instaloader.Post class, caption_mentions and
tagged_users are available for --only-if filters.
2017-09-10 12:43:34 +02:00
Alexander Graf 9a72f85a0e Documentation Enhancements 2017-09-08 10:35:38 +02:00
Alexander Graf 9ae7b23939 Release of version 3.0 2017-09-01 16:01:51 +02:00
Alexander Graf 9dc7456edb Let caption_hashtags return lowercased hashtags
This is more useful, since hashtags should be matched
case-insensitively.
2017-08-30 10:02:45 +02:00
Alexander Graf 12d34143c2 very minor documentation fixes 2017-08-30 09:50:26 +02:00
Alexander Graf 7862bdb173 First Release Candidate for Version 3.0 2017-08-29 12:19:27 +02:00
Alexander Graf e143096140 Minor documentation fixes 2017-08-29 12:13:38 +02:00
Alexander Graf 5b5d540310 caption_hashtags property for only-if evaluation
caption_hashtags is a list of all hashtags that are mentioned in the
Post's caption. It allows to easily filter Posts that have multiple
hashtags, and as such fixes #24.

Further, the documentation of --only-if has been completed by linking to
a description of the syntax in the Python documentation, and by linking
to a list of all defined properties with their meanings. So, this commit
also closes #42.
2017-08-29 11:03:12 +02:00
Alexander Graf d84136b2dd Minor doc-related improvements 2017-08-28 21:45:43 +02:00
Alexander Graf fbbbfdf53c Do not repeat errors that were fixed by retrying 2017-08-28 21:05:46 +02:00
Lars Lindqvist 2dbd510486 Ignore device_timestamp for stories.
I've come across several implausible values for `device_timestamp` (such as 182428140, which would be in october 1975, assuming millisecond). I guess it is due to improperly configured phones, or maybe some third party software that's mangeling the EXIF data on images before posting. Anyway. Since by the nature of stories, the `taken_at` timestamp (presumably when the instagram servers received the post) ought to be approximately when an image was actually taken. So there's no real value trying to use the timestamp provided by the photo-taking-device.
2017-08-27 22:51:30 +02:00
Alexander Graf e748b53b6a Add layout template extension
(forgot to commit it. Sorry...)
2017-08-26 13:49:54 +02:00
Alexander Graf 5aff8273b0 Minor tweaks to documentation 2017-08-26 12:42:04 +02:00
Alexander Graf 1f89dba702 Minor Sphinx theme switch
(The links sidebar was not shown on readthedocs.io)
2017-08-25 22:46:36 +02:00
Alexander Graf 90c5cc3c72 Minor documentation tunings 2017-08-25 22:38:54 +02:00
Alexander Graf d7b1e2002d Avoid unicode character in docs/conf.py
(caused problems on readthedocs)
2017-08-25 19:41:35 +02:00
Alexander Graf ec2e09edcf Create minimal documentation with Sphinx 2017-08-25 16:54:36 +02:00
André Koch-Kramer 1928db63bb Added ability to download videos within sidecars
Closes #41
2017-08-25 13:50:58 +02:00
Alexander Graf ca54088bdc Merge branch 'v3-dev' 2017-08-24 21:38:50 +02:00
André Koch-Kramer 1a26d7336c Fix TypeError on retry in get_json() 2017-08-24 21:17:49 +02:00
André Koch-Kramer bb71c40b56 Wait smarter to avoid HTTP error code 429
Additional sleeps are necessary because Instagram is rate limiting
GraphQL queries. The error does not occur if not more than 100 queries
are made in a sliding window of eleven minutes.
Ports a894c2d to version 3.
2017-08-24 18:30:46 +02:00
Alexander Graf 2e47642f74 Remove --shorter-output from --help 2017-08-24 17:51:39 +02:00
Alexander Graf 9939eab8ec Minor fixes to README.rst 2017-08-24 17:23:46 +02:00
Alexander Graf c61d6a93b2 Restructure README.rst 2017-08-24 17:10:32 +02:00
Alexander Graf bbdd3873e2 --skip-videos -> --no-videos; no --shorter-output 2017-08-24 16:03:24 +02:00
Alexander Graf 43bfd6b5ab Put all our GitHub keywords in setup.py 2017-08-24 15:54:07 +02:00
Alexander Graf dd99417e7b Require win_unicode_console on Windows Python 3.5 2017-08-24 15:40:41 +02:00
Alexander Graf 8cf1997460 Evaluate --only-if filter smarter
If --only-if='likes>1000 or viewer_has_liked' is given, it is not
neccessary to evaluate viewer_has_liked if the post has more than 1000
likes. The new implementation smartly handles this case.
2017-08-23 15:38:16 +02:00
Alexander Graf ce38f5880f Call download_stories in _error_catcher
download_stories() may trow a BadResponseException, which should not
cause abortion of download_profile(). Now, all calls of
download_stories() are within an _error_catcher context.
2017-08-22 09:21:47 +02:00
Lars Lindqvist 79f143b1f8 Minor stories-related cleanup 2017-08-20 20:32:45 +02:00
Alexander Graf ad34fc09b6 Wait longer after HTTP err 429 (Too Many Requests) 2017-08-20 11:48:19 +02:00
Alexander Graf 566ef02b94 Change sleep interval between requests
These are now adapted to how many requests have already been done. With
the current settings, Instaloader does not more than

12 request in the first ten seconds,
28 requests in the first minute,
40 requests in the first two minutes,
63 requests in the first five minutes,
90 requests in the first ten minutes,
and after that 50 requests per ten minutes.

This should make it less likely that Instaloader is rate-limited by
Instagram, while still being fast if downloading only a few posts.

Further, option --no-sleep is hidden in --help output and README.rst.
2017-08-20 11:31:46 +02:00
Alexander Graf 6300c217b3 Fixes to instaloader.Post metadata retrieval
Fixes a KeyError which occurred when fetching some information note
available in Post._node. Makes all properties not throw exceptions,
rather they return None if the information cannot be obtained.
sidecar_edges is now a method get_sidecar_edges(). get_comments() does
not do any additional requests if the post does not have any comment.

Fixes #39.
2017-08-20 10:37:58 +02:00
Alexander Graf d967400cb4 Fix downloading hashtags with unicode characters
Non-latin characters in the referer string used in the HTTP headers are
now properly quoted.
2017-08-19 22:44:08 +02:00
Alexander Graf 9ee98a2925 Use lowercase hashtags and profile names
Since both hashtags and profile names are case insensitive, this might
be a useful normalization and could workaround some user-induced bugs.
2017-08-19 18:26:42 +02:00
Alexander Graf ee9993d7c2 Filter posts with --only-if=FILTER
where FILTER is a boolean expression in python syntax where all names
are evaluated to instaloader.Post properties.

Examples:

instaloader --login=USER --only-if='viewer_has_liked' :feed

instaloader --only-if='likes>1000 and comments>5' profile
2017-08-19 17:54:23 +02:00
Lars Lindqvist 09d2592635 Use anonymous session for HEAD request.
The default Instaloader headers aren't passed with a simple `requests.head()`, so leakage of user agents such as `"python-requests/2.18.1"` will occur.
2017-08-19 15:17:43 +02:00
Alexander Graf 0f64768dd8 Post class representing an Instagram Post
This simplifies accessing properties of a Post. Method download_post()
remains to class Instaloader rather than Post, as it fits there better.

Also, since it is now easily possible, all download_*() functions now
have a filter_func parameter. Its meaning has been reverted to be
consistent of how a filter is commonly understood: A post is downloaded
iff filter_func is None or evaluates to True.

Post.get_comments() foreports commit 86fb80d ("Avoid GraphQL queries if
all comments in metadata").
2017-08-19 13:02:49 +02:00
Lars Lindqvist ccdac0305f Simplify profpic regex.
The original method of substituting 2048x2048 for whatever resolution was given seemed somewhat convoluted. This accomplishes the same thing, except raising an exception if the given url is not on the right domain.
2017-08-13 23:37:53 +02:00
André Koch-Kramer d28bd3ab52 Release of version 2.2.3 2017-08-13 14:39:35 +02:00
Alexander Graf 4ce6826f82 Don't retry downloads with 404 status
Instead of retrying a download attempt answered with a 404, the download
is aborted after the first attempt. Thanks to the _error_catcher(), a
message is printed and Instaloader goes on with the next files to
download.

Further, this commit removes the unneeded NodeUnavailableException and
adjusts docstrings accordingly.
2017-08-13 12:57:31 +02:00
Alexander Graf adeb588471 Fix KeyError when generating NodeUnavailable error
Fixes #32.
2017-08-13 12:51:39 +02:00
Alexander Graf 57329482f3 Evaluate status field in JSON response
In GraphQL queries, this field should be set to "ok". Checking this
addresses #31 (and maybe other issues as well) and simplifies error
handling.
2017-08-13 11:02:47 +02:00
André Koch-Kramer a894c2d206 Wait to avoid error code 429 (Too Many Requests)
Additional sleeps are necessary because Instagram is rate limiting
GraphQL queries. The error does not occur if not more than 100 queries
are made in a sliding window of eleven minutes.
Fixes #29
2017-08-12 19:57:54 +02:00
André Koch-Kramer 86fb80d7e4 Avoid GraphQL queries if all comments in metadata
If get_node_metadata() is able to provide all comments of a node, no
additional query is needed. Especially GraphQL queries are time
expensive because no more than 100 can be queried in ten minutes. Since
get_node_metadata() does not use GraphQL queries, this is a usefull
tradeoff.
+ Additional error handling
2017-08-12 19:04:22 +02:00
Alexander Graf dcc48e37df Minor fix regarding setup.py
Now, setup.py does not assume to be called from the path where the
source tree resides. This fixes getting the long_description and the
version if setup.py is called from outside.
2017-08-11 20:34:47 +02:00
Alexander Graf 117124d1dd Options --no-captions and --no-geotags
These options instruct instaloader to not save captions or geotags
respectively, even if the regarding information can be obtained without
any additional queries to Instagram.

This feature was proposed in #25, and thus this commit should close #25.
2017-08-11 19:54:10 +02:00
Alexander Graf 0d9af81ae7 Minor enhancements
Get rid of NonfatalException (an exception is nonfatal iff it is
catched somewhere)

Foreport fixes for #26 and #30.

The current __sersion__ string is now kept in instaloader.py rather than
setup.py. This lets instaloader.__version__ always deliver the version
of the actually-loaded Instaloader module.

Minor changes to README.rst, error handling and which class methods are
public.

With these and the changes of the previous commit, we saved 31 lines of
code, indicating that it might be easier to understand and to maintain.
2017-08-11 18:20:58 +02:00
Alexander Graf 9eddc03cf1 Don't rely on fromtimestamp() raising ValueError
Instead of relying on that datetime.fromtimestamp() raises ValueError if
the timestamp of a story is in milliseconds rather than seconds, we now
compare the timestamp value with a timestamp of the year 2286 to decide
whether to divide it by 1000 or not.

This is motivated by #30, where an OSError is raised in
datetime.fromtimestamp() under Windows.
2017-08-09 18:27:05 +02:00
André Koch-Kramer 42864997b3 get_node_metadata() now also catches TypeError
Concerns #26
2017-08-08 00:43:52 +02:00
Alexander Graf 58882f508e Major code cleanup
Remove many code duplications, merely by using more pythonic idioms.

Use GraphQL more often.

Better cope with errors: All requests can be retried; failed requests do
not cause program termination; all error strings are repeated to the
user at the end of execution.

download_post() (formerly download_node()) does not repeat node metadata
request (before this commit, this request was executed up to three
times).
2017-08-06 19:27:46 +02:00
Alexander Graf 5d83a4ccf6 Release of version 2.2.2 2017-08-06 10:49:36 +02:00
Alexander Graf 56c8ac729f Cleanup .gitignore 2017-08-05 13:43:26 +02:00
André Koch-Kramer 987d95c048 Forgot "file=" in some print statements 2017-08-05 00:22:43 +02:00
André Koch-Kramer 9b5d4e34fc Raise and catch NodeUnavailableException
In case a node can not be downloaded or its metadata is needed and can
not be retrieved, a NodeUnavailableException is raised and the according
node will be skipped.
Concerns #26
2017-08-04 19:19:56 +02:00
Alexander Graf 838ea645a8 Let Travis automatically deploy releases to PyPi 2017-08-04 11:18:20 +02:00
Alexander Graf 01fc150e78 Release of version 2.2.1 2017-08-03 07:33:38 +02:00
Alexander Graf 46ac119a10 Fix AttributeError when not logging in
Mentioned in #26. Bug was introduced in 0ad50c1.
2017-08-03 07:31:58 +02:00
André Koch-Kramer 6b520f6a2d Release of version 2.2 2017-08-01 16:41:42 +02:00
André Koch-Kramer c3a5557140 Additionally catch HTTPError and RequestException
Concerns issue #26.
2017-08-01 16:30:59 +02:00
André Koch-Kramer 0ad50c1526 Fix error on downloading own private profile
Closes #27.
2017-07-31 21:18:42 +02:00
André Koch-Kramer 9fbe9b0903 Retry get requests for downloading pictures
Tries to workaround #26.
2017-07-31 20:34:27 +02:00
Alexander Graf 4b8b257672 Fix rst formatting in README 2017-07-29 21:51:51 +02:00
Alexander Graf b1b90f8abf target :stories; flags --stories & --stories-only
This allows to invoke the new download_stories() function contributed
in #28 by command line.
2017-07-29 17:51:39 +02:00
Alexander Graf 66f69b5c21 Consistently use datetime type for handling dates 2017-07-29 11:08:52 +02:00
André Koch-Kramer 48d6f0226b Added Disclaimer to README 2017-07-29 05:24:48 +02:00
André Koch-Kramer 82ae31cea5 Fix download_stories() to get all available posts
download_stories() did and does not check if a story is "unseen". The
response to the query of the '/feed/reels_tray/' URL provides all
available stories of the user's followees. Nevertheless, some of them do
not contain an 'items' field which has no causal relationship with their
status in terms of "seen" or "unseen". Therefore, to overcome this lack
of 'items' the 'feed/user/TARGET_USERID/reel_media/' URL needs to be
queried for each relevant followee whose 'items' were not provided in
the first place.
2017-07-29 04:12:26 +02:00
André Koch-Kramer 3e0b81ad56 copy_session() now also copies session.headers
Store a shallow copy of the headers rather than just create bindings
between the headers of the original and the newly created session object.
2017-07-29 01:54:42 +02:00
André Koch-Kramer 99620ec766 Allow subdirectories in filename pattern
Changed invocations of os.makedirs() in order to respect directories in
the filename pattern.
2017-07-29 01:40:53 +02:00
André Koch-Kramer 13d288ef36 Minor fixes of download_stories()
Concerning pull request #28.
2017-07-28 19:49:48 +02:00
Lars Lindqvist b85e8fd793 Get stories per user 2017-07-28 18:36:15 +02:00
André Koch-Kramer 1699954761 Disable some trolling pylint features 2017-07-27 22:26:33 +02:00
André Koch-Kramer 7cac6d53f2 Minor code adaptions for consistency reasons
Concerning pull request #28.
2017-07-27 22:18:43 +02:00
Lars Lindqvist c355338010 Stories POC 2017-07-27 16:59:21 +02:00
André Koch-Kramer 2205bbcf45 Release of version 2.1 2017-07-26 20:06:34 +02:00
André Koch-Kramer c299e9d1a2 Updated README + {date} default format added 2017-07-26 19:13:56 +02:00
Alexander Graf 151ccfd71d Enhance behavior of waiting between requests
Unless --no-sleep is given, Instaloader waits between requests to the
Instagram servers.

This commit fixes and enhances this behavior. Now, --no-sleep is always
obeyed. Between requests to the instagram.com servers, there is now a
delay of 250 ms ~ 2000 ms. Requests to the file servers do not cause a
delay.
2017-07-26 15:10:12 +02:00
André Koch-Kramer bf5e3b90d0 Updated descriptions consistent with docstring 2017-07-26 14:43:08 +02:00
Alexander Graf 56b27fb26f Fix download_feed_pics()
Now we also use GraphQL queries for retrieving the user's feed.
2017-07-25 21:01:48 +02:00
Alexander Graf 8572e527ec Options --dirname-pattern and --filename-pattern
Instaloader downloads all posts in

  <DIRNAME>/<FILENAME>+(suffix and extension)

which are now generated by the templates given with --dirname-pattern
and --filename-pattern. These templates may contain specifiers such as
'{target}', '{profile}', '{date}' and '{shortcode}'.

Default for --dirname-pattern is '{target}', default for
--filename-pattern is '{date:%Y-%m-%d_%H-%M-%S}'

The former options --no-profile-subdir and --hashtag-username were
removed, because their behavior can now be achieved like this:

--no-profile-subdir and --hashtag-username:
--dirname-pattern='.' --filename-pattern='{profile}__{date:%Y-%m-%d_%H-%M-%S}'

--no-profile-subdir, but not --hashtag-username:
--dirname-pattern='.' --filename-pattern='{target}__{date:%Y-%m-%d_%H-%M-%S}'

--hashtag-username but not --no-profile-subdir:
--dirname-pattern='{profile}'

This adds the option proposed in #23, to encode both the hashtag and the
profile name in the file's path when downloading by hashtag, e.g.:
--dirname-pattern='{target}' --filename-pattern='{profile}_{date:%Y-%m-%d_%H-%M-%S}'

(Closes #23)
2017-07-25 18:45:01 +02:00
Alexander Graf 5068c9453e graphql_query() method for GraphQL queries 2017-07-24 12:08:08 +02:00
André Koch-Kramer d614d9f015 Release of version 2.0 2017-07-22 18:43:54 +02:00
André Koch-Kramer 051a6fa9d0 args.comments 2017-07-21 15:32:41 +02:00
André Koch-Kramer 169ce1a300 Download comments
Close #5
2017-07-20 22:36:30 +02:00
Alexander Graf ee8e159d56 Fix README regarding when profiles are found by ID 2017-07-20 18:19:15 +02:00
Alexander Graf b3f916b371 Fix pathname capitalization inconsistency issues 2017-07-20 18:08:16 +02:00
André Koch-Kramer 1fdce16f46 Fix get_followees() and implement get_followers() 2017-07-20 18:01:29 +02:00
Alexander Graf dd513f7190 Let anonymous loader inherit all options 2017-07-20 15:24:57 +02:00
Alexander Graf 7198f1ad9f Restructure --help and Options section in README 2017-07-20 14:54:22 +02:00
Alexander Graf 58c12d5618 Allow changing HTTP User Agent string 2017-07-20 11:25:46 +02:00
Alexander Graf 1e10ab8669 Revert "Replaced usages of shortcode with mediaid"
This reverts commit 715582138b.

It broke downloading sidecars and did not introduce any advantageous
behavior.
2017-07-14 15:29:09 +02:00
Alexander Graf c0eecd1bd2 Usability fixes in improvements
On module level:

Cleaner exception handling for load_session_from file

interactive_login logs in interactively now, always asking the user for
password. Before, it had an optional password parameter determining
whether it was interactive or not.

On application level:

Warn if profile specifiers are used which require login, but not --login
flag was given (@profile, :feed-all, :feed-liked).

Clearly warn that --password is insecure.
2017-07-14 11:04:32 +02:00
André Koch-Kramer 8607135740 Satisfy pylint 2017-07-14 05:37:36 +02:00
André Koch-Kramer ca2829becc Fixed and reimplemented get_username_by_id() 2017-07-14 05:18:18 +02:00
André Koch-Kramer 715582138b Replaced usages of shortcode with mediaid 2017-07-13 22:33:01 +02:00
André Koch-Kramer 184c521646 Added functions to convert mediaid <-> shortcode 2017-07-06 22:26:25 +02:00
André Koch-Kramer 5ae3d7090f Minor bug fixes
- Adjust comment in test()
- Added exception handling when loading a sessionfile
- Corrected control flow in interactive_login()
2017-06-30 15:45:38 +02:00
Alexander Graf 4768fdbd10 --hashtag-username to store by-username
With --hashtag-username given, if downloading per #hashtag, instead of
per username, for each picture an additional request to the Instagram
server is issued to lookup the picture's username. Instead of storing
files in #hashtag/timestamp.jpg, files are stored in
username/timestamp.jpg as it is the default when not downloading per
hashtag.

This closes #22.
2017-06-27 09:19:29 +02:00
Alexander Graf 591dfd31e4 --no-profile-subdir to encode profile in filename
Fixes #22.
2017-06-25 14:55:44 +02:00
Alexander Graf caf75a8135 Refactor Instaloader's methods into a class 2017-06-24 22:43:40 +02:00
André Koch-Kramer 52492456ed Release of version 1.3 2017-04-22 18:01:54 +02:00
Alexander Graf e0924e8d08 Fix --geotags 2017-04-22 17:54:21 +02:00
Alexander Graf 655dbb552d Further clarify meaning of --count 2017-04-22 17:34:49 +02:00
André Koch-Kramer fdb8e94c64 Fixed ":feed-liked" functionality 2017-04-22 17:26:48 +02:00
Alexander Graf b3c83f420c --count to limit posts at #hashtag and :feed-* 2017-04-22 17:21:02 +02:00
Alexander Graf df00e2b03a Minor enhancements to README 2017-04-22 12:11:56 +02:00
Alexander Graf b60f0b4836 README: Document all of Instaloader's features 2017-04-22 11:58:59 +02:00
Alexander Graf 35e71192c5 travis: Enable more Pylint inspections 2017-04-22 11:58:12 +02:00
André Koch-Kramer 2106c2d5f6 Satisfy pylint after their update 2017-04-22 11:13:10 +02:00
André Koch-Kramer 0e943189e5 Parse graphql structure for sidecars 2017-04-22 10:50:12 +02:00
André Koch-Kramer 8e77a1c125 Fixed download_node() 2017-04-22 01:39:52 +02:00
André Koch-Kramer 361445519a Adapt new graphql layout used by Instagram
Fixes #19.
2017-04-21 18:10:41 +02:00
Alexander Graf 6b345e1f52 Adapt video downloading to new format
This should the video downloading issue reported at #18.
2017-04-20 09:17:59 +02:00
Alexander Graf c841bca81b Silence new pylint warnings 2017-04-20 09:17:32 +02:00
Alexander Graf 3e1360160d Download pictures with #hashtag
Instaloader is now capable of downloading all pictures associated with
one #hashtag with:
instaloader #hashtag

This implements the feature requested with #18.
2017-04-17 12:16:22 +02:00
Alexander Graf a7d1c5bbb0 Make check_id() exceptions Non-fatal
The check_id() step, including the get_username_by_id(), which is used
to determine whether a profile's account name has changed since the last
download using it's unique id when Instaloader is operating logged-in,
is actually optional and should not cause termination in any case.
2017-04-10 21:05:58 +02:00
Alexander Graf dc748a0541 Download all pictures of Sidecar nodes 2017-03-25 21:08:54 +01:00
Alexander Graf 72c647829a Don't fail if --sessionfile does not contain '/' 2017-03-21 14:58:13 +01:00
Alexander Graf 0a039b9c8c Release of version 1.2.2 2017-03-19 14:52:45 +01:00
Alexander Graf b1014bfbfd README: Mention how to install from our Git repo 2017-03-19 14:42:02 +01:00
Alexander Graf d246268630 Retry download anonymously if profile not exists
In case you are blocked by a public profile which you intend to
download, the server responds as if the profile would not exist. Now in
this case, we retry the download without using an anonymous session.
2017-03-19 12:52:07 +01:00
Alexander Graf 23a0e32e8e Clarify --login is required for download followees 2017-03-19 12:51:20 +01:00
79 changed files with 12436 additions and 839 deletions
+32
View File
@@ -0,0 +1,32 @@
---
name: 🐛 Bug report
about: Create a report to help us improve 🤕
labels: bug
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
(e.g. Instaloader command line)
**Expected behavior**
A clear and concise description of what you expected to happen (even if it seems obvious).
**Error messages and tracebacks**
If applicable, add error messages and tracebacks to help explain your problem.
**Instaloader version**
`instaloader --version` output.
**Additional context**
Add any other context about the problem here.
<!--
Also see https://instaloader.github.io/contributing.html for how to report a bug.
Please understand that a bug issue that lacks the required information cannot be
processed.
-->
+11
View File
@@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: Contributing to Instaloader
url: https://instaloader.github.io/contributing.html
about: Guidelines for how to report an issue.
- name: Instaloader Troubleshooting
url: https://instaloader.github.io/troubleshooting.html
about: Many common problems and their workarounds are described here.
- name: Instaloader Documentation
url: https://instaloader.github.io/
about: Many questions might be already answered here.
+31
View File
@@ -0,0 +1,31 @@
---
name: 🚀 Feature Suggestion
about: Suggest an idea for this project 💡
labels: "feature suggestion"
---
**Provide us a use case of the feature**
How could the user invoke the new function? Which problem would it solve?
If new information is obtained, how would it be further processed?
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
How do you achieve the here-described goal without the feature you are requesting?
**If the feature request is accepted, would you be willing to submit a pull request?**
Yes / No
<!-- Help can be provided if you need assistance submitting a PR -->
**Additional context**
Add any other context about the feature request here.
<!--
Also see https://instaloader.github.io/contributing.html for how to suggest a feature.
Please understand that a feature suggestion issue that lacks the required information cannot be
processed.
-->
+18
View File
@@ -0,0 +1,18 @@
---
name: ❓ Question
about: If you have a question about how to use Instaloader that's not well covered by our documentation
labels: question
---
Your question here...
<!--
People will be better able and more motivated to provide help if your question
- has a concise and meaningful title,
- is not covered by the documentation: https://instaloader.github.io/,
- is actually related to Instaloader (this is not a Python help forum),
- has not already been answered here or on other sites such as Stack Overflow: https://stackoverflow.com/questions/tagged/instaloader,
- is written in a polite and welcoming tone.
-->
+15
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
github: [aandergr]
custom: "https://instaloader.github.io/sponsors.html"
+24
View File
@@ -0,0 +1,24 @@
<!--
Thanks for your willingness to contribute to Instaloader! Please briefly
describe:
- A motivation for this change, e.g.
- Fixes # .
- What problem does the pull request solve?
- The changes proposed in this pull request
- Keep the PR focused: avoid bundling independent fixes or refactorings.
- The completeness of this change
- Is the documentation updated (if appropriate)?
- How has it been tested?
- Do you consider it ready to be merged or is it a draft / proof of concept?
- Can we help you at some point?
- Further considerations
- Is it based on master (bugfix), upcoming/v4.X (new feature), or another pull request?
- Does it preserve backward compatibility and existing behavior?
-->
+33
View File
@@ -0,0 +1,33 @@
name: "Documentation"
on: [push, pull_request]
jobs:
docs:
name: "Documentation"
runs-on: ubuntu-latest
steps:
- name: "Checkout Instaloader Repository"
uses: actions/checkout@v7
with:
fetch-depth: 0 # needed for building docs
persist-credentials: false
- name: "Setup Python"
uses: actions/setup-python@v7
with:
python-version: 3.15
cache: 'pipenv'
- name: "Install Dependencies"
run: |
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"
- name: "Deploy Documentation"
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: master
repository-name: instaloader/instaloader.github.io
folder: docs/_build/html
token: ${{ secrets.PAGES_DEPLOYMENT_TOKEN }}
+30
View File
@@ -0,0 +1,30 @@
name: PyLint and MyPy
on: [push, pull_request]
jobs:
lint:
name: PyLint and MyPy
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.15"]
steps:
- name: Checkout Instaloader Repository
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Setup Python
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
cache: 'pipenv'
- name: Install Dependencies
run: |
python -m pip install pipenv==2026.8.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
- name: MyPy
run: pipenv --python `python --version | grep -Eo '3\.[0-9]+'` run mypy -m instaloader
+40
View File
@@ -0,0 +1,40 @@
name: "Publish Packages"
on:
push:
tags:
- v*
jobs:
publish:
name: "Publish Package"
runs-on: ubuntu-latest
steps:
- name: "Checkout Repository"
uses: actions/checkout@v7
with:
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:
GITHUB_REF: ${{ github.ref }}
run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
shell: bash
- name: "Install Dependencies"
run: python -m pip install -U setuptools wheel build
- name: "Generate Python Package"
run: python -m build
- name: "Publish Distribution to PyPI"
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_PASSWORD }}
- name: "Publish Distribution to AUR"
run: deploy/arch/deploy.sh $VERSION_TAG
env:
VERSION_TAG: v${{ steps.get_version.outputs.VERSION }}
AUR_KEY: ${{ secrets.AUR_KEY }}
AUR_IV: ${{ secrets.AUR_IV }}
+34
View File
@@ -0,0 +1,34 @@
name: Mark stale issues and pull requests
on:
schedule:
- cron: "55 7 * * *"
jobs:
stale:
name: Mark stale issues and pull requests
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v11
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
only-labels: 'question'
stale-issue-message: 'There has been no activity on this question for an extended period of time. This issue will be closed after further 14 days of inactivity.'
stale-issue-label: 'stale'
exempt-issue-labels: 'leave open'
days-before-stale: 21
days-before-close: -1
remove-stale-when-updated: false
- 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.'
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-labels: 'leave open'
exempt-pr-labels: 'leave open'
days-before-stale: 189
days-before-close: 14
+41
View File
@@ -0,0 +1,41 @@
name: Windows EXE build
on: [push, pull_request]
jobs:
build:
name: Windows EXE build
runs-on: windows-latest
steps:
- name: Checkout Instaloader repository
uses: actions/checkout@v7
with:
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
id: get_version
env:
GITHUB_REF: ${{ github.ref }}
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 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@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ 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')
files: ./instaloader-v${{ steps.get_version.outputs.VERSION }}-windows-standalone.zip
+1 -3
View File
@@ -1,4 +1,2 @@
__pycache__/
build/
dist/
instaloader.egg-info/
.mypy_cache/
+647
View File
@@ -0,0 +1,647 @@
[MAIN]
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Clear in-memory caches upon conclusion of linting. Useful if running pylint
# in a server-like mode.
clear-cache-post-run=no
# Load and enable all available extensions. Use --list-extensions to see a list
# all available extensions.
#enable-all-extensions=
# In error mode, messages with a category besides ERROR or FATAL are
# suppressed, and no reports are done by default. Error mode is compatible with
# disabling specific errors.
#errors-only=
# Always return a 0 (non-error) status code, even if lint errors are found.
# This is primarily useful in continuous integration scripts.
#exit-zero=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code.
extension-pkg-allow-list=
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
# for backward compatibility.)
extension-pkg-whitelist=
# Return non-zero exit code if any of these messages/categories are detected,
# even if score is above --fail-under value. Syntax same as enable. Messages
# specified are enabled, while categories only check already-enabled messages.
fail-on=
# Specify a score threshold under which the program will exit with error.
fail-under=10
# Interpret the stdin as a python script, whose filename needs to be passed as
# the module_or_package argument.
#from-stdin=
# Files or directories to be skipped. They should be base names, not paths.
ignore=CVS
# Add files or directories matching the regular expressions patterns to the
# ignore-list. The regex matches against paths and can be in Posix or Windows
# format. Because '\\' represents the directory delimiter on Windows systems,
# it can't be used as an escape character.
ignore-paths=
# Files or directories matching the regular expression patterns are skipped.
# The regex matches against base names, not paths. The default value ignores
# Emacs file locks
ignore-patterns=^\.#
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
# number of processors available to use, and will cap the count on Windows to
# avoid hangs.
jobs=1
# Control the amount of potential inferred values when inferring a single
# object. This can help the performance when dealing with large functions or
# complex, nested conditions.
limit-inference-results=100
# List of plugins (as comma separated values of python module names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Minimum Python version to use for version dependent checks. Will default to
# the version used to run pylint.
py-version=3.8
# Discover python modules and packages in the file system subtree.
recursive=no
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# In verbose mode, extra non-checker-related info will be displayed.
#verbose=
[BASIC]
# Naming style matching correct argument names.
argument-naming-style=snake_case
# Regular expression matching correct argument names. Overrides argument-
# naming-style. If left empty, argument names will be checked with the set
# naming style.
#argument-rgx=
# Naming style matching correct attribute names.
attr-naming-style=snake_case
# Regular expression matching correct attribute names. Overrides attr-naming-
# style. If left empty, attribute names will be checked with the set naming
# style.
#attr-rgx=
# Bad variable names which should always be refused, separated by a comma.
bad-names=foo,
bar,
baz,
toto,
tutu,
tata
# Bad variable names regexes, separated by a comma. If names match any regex,
# they will always be refused
bad-names-rgxs=
# Naming style matching correct class attribute names.
class-attribute-naming-style=any
# Regular expression matching correct class attribute names. Overrides class-
# attribute-naming-style. If left empty, class attribute names will be checked
# with the set naming style.
#class-attribute-rgx=
# Naming style matching correct class constant names.
class-const-naming-style=UPPER_CASE
# Regular expression matching correct class constant names. Overrides class-
# const-naming-style. If left empty, class constant names will be checked with
# the set naming style.
#class-const-rgx=
# Naming style matching correct class names.
class-naming-style=PascalCase
# Regular expression matching correct class names. Overrides class-naming-
# style. If left empty, class names will be checked with the set naming style.
#class-rgx=
# Naming style matching correct constant names.
const-naming-style=UPPER_CASE
# Regular expression matching correct constant names. Overrides const-naming-
# style. If left empty, constant names will be checked with the set naming
# style.
#const-rgx=
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming style matching correct function names.
function-naming-style=snake_case
# Regular expression matching correct function names. Overrides function-
# naming-style. If left empty, function names will be checked with the set
# naming style.
#function-rgx=
# Good variable names which should always be accepted, separated by a comma.
good-names=i,
j,
k,
ex,
Run,
_
# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
good-names-rgxs=
# Include a hint for the correct naming format with invalid-name.
include-naming-hint=no
# Naming style matching correct inline iteration names.
inlinevar-naming-style=any
# Regular expression matching correct inline iteration names. Overrides
# inlinevar-naming-style. If left empty, inline iteration names will be checked
# with the set naming style.
#inlinevar-rgx=
# Naming style matching correct method names.
method-naming-style=snake_case
# Regular expression matching correct method names. Overrides method-naming-
# style. If left empty, method names will be checked with the set naming style.
#method-rgx=
# Naming style matching correct module names.
module-naming-style=snake_case
# Regular expression matching correct module names. Overrides module-naming-
# style. If left empty, module names will be checked with the set naming style.
#module-rgx=
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
# These decorators are taken in consideration only for invalid-name.
property-classes=abc.abstractproperty
# Regular expression matching correct type variable names. If left empty, type
# variable names will be checked with the set naming style.
#typevar-rgx=
# Naming style matching correct variable names.
variable-naming-style=snake_case
# Regular expression matching correct variable names. Overrides variable-
# naming-style. If left empty, variable names will be checked with the set
# naming style.
#variable-rgx=
[CLASSES]
# Warn about protected attribute access inside special methods
check-protected-access-in-special-methods=no
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,
__new__,
setUp,
__post_init__
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,
_fields,
_replace,
_source,
_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[DESIGN]
# List of regular expressions of class ancestor names to ignore when counting
# public methods (see R0903)
exclude-too-few-public-methods=
# List of qualified class names to ignore when counting class parents (see
# R0901)
ignored-parents=
# Maximum number of arguments for function / method.
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Maximum number of boolean expressions in an if statement (see R0916).
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=12
# Maximum number of locals for function / method body.
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body.
max-returns=6
# Maximum number of statements in function / method body.
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[EXCEPTIONS]
# Exceptions that will emit a warning when caught.
overgeneral-exceptions=builtins.BaseException,builtins.Exception
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=120
# Maximum number of lines in a module.
max-module-lines=1000
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[IMPORTS]
# List of modules that can be imported at any level, not just the top level
# one.
allow-any-import-level=
# Allow explicit reexports by alias from a package __init__.
allow-reexport-from-package=yes
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Deprecated modules which should not be used, separated by a comma.
deprecated-modules=
# Output a graph (.gv or any supported image format) of external dependencies
# to the given file (report RP0402 must not be disabled).
ext-import-graph=
# Output a graph (.gv or any supported image format) of all (i.e. internal and
# external) dependencies to the given file (report RP0402 must not be
# disabled).
import-graph=
# Output a graph (.gv or any supported image format) of internal dependencies
# to the given file (report RP0402 must not be disabled).
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
# Couples of modules and preferred modules, separated by a comma.
preferred-modules=
[LOGGING]
# The type of string formatting that logging methods do. `old` means using %
# formatting, `new` is for `{}` formatting.
logging-format-style=old
# Logging modules to check that the string format arguments are in logging
# function parameter format.
logging-modules=logging
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
# UNDEFINED.
confidence=HIGH,
CONTROL_FLOW,
INFERENCE,
INFERENCE_FAILURE,
UNDEFINED
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once). You can also use "--disable=all" to
# disable everything first and then re-enable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use "--disable=all --enable=classes
# --disable=W".
disable=bad-inline-option,
consider-using-f-string,
cyclic-import,
deprecated-pragma,
file-ignored,
invalid-name,
locally-disabled,
missing-class-docstring,
missing-function-docstring,
missing-module-docstring,
no-else-raise,
no-else-return,
raw-checker-failed,
suppressed-message,
too-many-arguments,
too-many-branches,
too-many-instance-attributes,
too-many-lines,
too-many-locals,
too-many-positional-arguments,
too-many-public-methods,
too-many-statements,
unnecessary-lambda-assignment,
unnecessary-pass,
unspecified-encoding,
unused-wildcard-import,
use-dict-literal,
use-list-literal,
use-symbolic-message-instead,
useless-suppression,
wildcard-import
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=c-extension-no-member
[METHOD_ARGS]
# List of qualified names (i.e., library.method) which require a timeout
# parameter e.g. 'requests.api.get,requests.api.post'
timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,
XXX,
TODO
# Regular expression of note tags to take in consideration.
notes-rgx=
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
# Complete name of functions that never returns. When checking for
# inconsistent-return-statements if a never returning function is called then
# it will be considered as an explicit return statement and no message will be
# printed.
never-returning-functions=sys.exit,argparse.parse_error
[REPORTS]
# Python expression which should return a score less than or equal to 10. You
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
# 'convention', and 'info' which contain the number of messages in each
# category, as well as 'statement' which is the total number of statements
# analyzed. This score is used by the global evaluation report (RP0004).
evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details.
msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
#output-format=
# Tells whether to display a full report or only the messages.
reports=no
# Activate the evaluation score.
score=yes
[SIMILARITIES]
# Comments are removed from the similarity computation
ignore-comments=yes
# Docstrings are removed from the similarity computation
ignore-docstrings=yes
# Imports are removed from the similarity computation
ignore-imports=yes
# Signatures are removed from the similarity computation
ignore-signatures=yes
# Minimum lines number of a similarity.
min-similarity-lines=4
[SPELLING]
# Limits count of emitted suggestions for spelling mistakes.
max-spelling-suggestions=4
# Spelling dictionary name. Available dictionaries: none. To make it work,
# install the 'python-enchant' package.
spelling-dict=
# List of comma separated words that should be considered directives if they
# appear at the beginning of a comment and should not be checked.
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains the private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to the private dictionary (see the
# --spelling-private-dict-file option) instead of raising a message.
spelling-store-unknown-words=no
[STRING]
# This flag controls whether inconsistent-quotes generates a warning when the
# character used as a quote delimiter is used inconsistently within a module.
check-quote-consistency=no
# This flag controls whether the implicit-str-concat should generate a warning
# on implicit string concatenation in sequences defined over several lines.
check-str-concat-over-line-jumps=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether to warn about missing members when the owner of the attribute
# is inferred to be None.
ignore-none=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of symbolic message names to ignore for Mixin members.
ignored-checks-for-mixins=no-member,
not-async-context-manager,
not-context-manager,
attribute-defined-outside-init
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
# Regex pattern to define which classes are considered mixins.
mixin-class-rgx=.*[Mm]ixin
# List of decorators that change the signature of a decorated function.
signature-mutators=
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid defining new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of names allowed to shadow builtins
allowed-redefined-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,
_cb
# A regular expression matching the name of dummy variables (i.e. expected to
# not be used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored.
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

-12
View File
@@ -1,12 +0,0 @@
language: python
python:
- "3.5"
- "3.5-dev"
- "3.6"
- "3.6-dev"
- "3.7-dev"
- "nightly"
install:
- pip install pylint requests
script:
- python3 -m pylint -r n -d bad-whitespace,bad-continuation,missing-docstring,multiple-imports,too-many-arguments,locally-disabled,line-too-long,invalid-sequence-index instaloader
+10
View File
@@ -0,0 +1,10 @@
Authors
=======
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
+135
View File
@@ -0,0 +1,135 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement by opening an
issue or contacting one or more of the project maintainers.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
at [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2016 Alexander Graf and André Koch-Kramer.
Copyright (c) 2016-2020 Alexander Graf and André Koch-Kramer.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+23
View File
@@ -0,0 +1,23 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
pylint = "*"
mypy = "*"
sphinx = "*"
pyinstaller = "*"
psutil = "*"
types-requests = "*"
sphinx-autodoc-typehints = "*"
sphinxcontrib-jquery = "*"
tomli = "*" # Needed for Pylint for "python_version < '3.11'"
dill = "*" # Needed for Pylint for "python_version < '3.11'"
pywin32-ctypes = "*" # Needed for pyinstaller on Windows
pefile = "*" # Needed for pyinstaller on Windows
pip-licenses = "*"
[packages]
requests = "*"
browser-cookie3 = "*"
Generated
+1477
View File
File diff suppressed because it is too large Load Diff
+114 -78
View File
@@ -1,43 +1,70 @@
Instaloader
===========
.. image:: https://raw.githubusercontent.com/instaloader/instaloader/master/docs/logo_heading.png
.. badges-start
Installation
------------
|pypi| |pyversion| |license| |aur| |contributors| |downloads|
Instaloader is written in Python, thus ensure having
`Python <https://www.python.org/>`__ (at least version 3.5) installed.
.. |pypi| image:: https://img.shields.io/pypi/v/instaloader.svg
:alt: Instaloader PyPI Project Page
:target: https://pypi.org/project/instaloader/
If you intend to use this tool under Windows, it is recommended
to install
`win-unicode-console <https://pypi.python.org/pypi/win_unicode_console>`__.
.. |license| image:: https://img.shields.io/github/license/instaloader/instaloader.svg
:alt: MIT License
:target: https://github.com/instaloader/instaloader/blob/master/LICENSE
If you have `pip <https://pypi.python.org/pypi/pip>`__ installed, you
may install Instaloader using
.. |pyversion| image:: https://img.shields.io/pypi/pyversions/instaloader.svg
:alt: Supported Python Versions
.. |contributors| image:: https://img.shields.io/github/contributors/instaloader/instaloader.svg
:alt: Contributor Count
:target: https://github.com/instaloader/instaloader/graphs/contributors
.. |aur| image:: https://img.shields.io/aur/version/instaloader.svg
:alt: Arch User Repository Package
:target: https://aur.archlinux.org/packages/instaloader/
.. |downloads| image:: https://pepy.tech/badge/instaloader/month
:alt: PyPI Download Count
:target: https://pepy.tech/project/instaloader
.. badges-end
::
pip3 install instaloader
$ pip3 install instaloader
Alternatively, you may `download Instaloader from
GitHub <https://github.com/Thammus/instaloader/releases>`__, unzip it
and invoke bundled ``setup.py`` (requiring
`setuptools <https://pypi.python.org/pypi/setuptools>`__) to install it:
$ instaloader profile [profile ...]
**Instaloader**
- downloads **public and private profiles, hashtags, user stories,
feeds and saved media**,
- downloads **comments, geotags and captions** of each post,
- automatically **detects profile name changes** and renames the target
directory accordingly,
- allows **fine-grained customization** of filters and where to store
downloaded media,
- automatically **resumes previously-interrupted** download iterations.
::
python3 setup.py install [--user]
instaloader [--comments] [--geotags]
[--stories] [--highlights] [--tagged] [--reels] [--igtv]
[--login YOUR-USERNAME] [--fast-update]
profile | "#hashtag" | :stories | :feed | :saved
(pass ``--user`` to install it for your user only instead of globally)
`Instaloader Documentation <https://instaloader.github.io/>`__
Instaloader requires
`requests <https://pypi.python.org/pypi/requests>`__, which
will be installed automatically by setup.py, if not already installed.
How to automatically download pictures from Instagram
How to Automatically Download Pictures from Instagram
-----------------------------------------------------
To download a set of profiles, do
To **download all pictures and videos of a profile**, as well as the
**profile picture**, do
::
@@ -46,14 +73,28 @@ To download a set of profiles, do
where ``profile`` is the name of a profile you want to download. Instead
of only one profile, you may also specify a list of profiles.
To later update your local copy of that profile, you may run
To later **update your local copy** of that profiles, you may run
::
instaloader --fast-update profile [profile ...]
When ``--fast-update`` is given, Instaloader terminates when arriving at
the first already-downloaded picture.
If ``--fast-update`` is given, Instaloader stops when arriving at the
first already-downloaded picture.
Alternatively, you can use ``--latest-stamps`` to have Instaloader store
the time each profile was last downloaded and only download newer media:
::
instaloader --latest-stamps -- profile [profile ...]
With this option it's possible to move or delete downloaded media and still keep
the archive updated.
When updating profiles, Instaloader
automatically **detects profile name changes** and renames the target directory
accordingly.
Instaloader can also be used to **download private profiles**. To do so,
invoke it with
@@ -62,77 +103,72 @@ invoke it with
instaloader --login=your_username profile [profile ...]
When invoked like this, it also **stores the session cookies** in a file
in ``/tmp``, which will be reused later when ``--login`` is given. So
you can download private profiles **non-interactively** when you already
have a valid session cookie file.
When logging in, Instaloader **stores the session cookies** in a file in your
temporary directory, which will be reused later the next time ``--login``
is given. So you can download private profiles **non-interactively** when you
already have a valid session cookie file.
If you want to **download all followees of a given profile**, call
`Instaloader Documentation <https://instaloader.github.io/basic-usage.html>`__
::
Contributing
------------
instaloader --login=your_username @profile
As an open source project, Instaloader heavily depends on the contributions from
its community. See
`contributing <https://instaloader.github.io/contributing.html>`__
for how you may help Instaloader to become an even greater tool.
To **download all the pictures from your feed which you have liked**, call
Supporters
----------
::
.. current-sponsors-start
instaloader --login=your_username :feed-liked
| Instaloader is proudly sponsored by
The ``--quiet`` option makes it also **suitable as a cron job**.
.. list-table::
:align: center
To get a list of other helpful flags, run ``instaloader --help``.
* - |socialapis|
* - `SocialAPIs <https://socialapis.io/?utm_source=github&utm_medium=sponsor&utm_campaign=instaloader>`__
Usage as library
----------------
|
You may also use parts of Instaloader as library to do other interesting
things.
.. list-table::
:align: center
For example, to get a list of all followers of a profile as well as
their follower count, do
* - `@rocketapi-io <https://github.com/rocketapi-io>`__
.. code:: python
|
import instaloader
See `Alex' GitHub Sponsors <https://github.com/sponsors/aandergr>`__ page for
how you can sponsor the development of Instaloader!
# login
session = instaloader.get_logged_in_session(USERNAME)
.. |socialapis| image:: ./.sponsor-logos/socialapis.png
:alt: SocialAPIs
:target: https://socialapis.io/?utm_source=github&utm_medium=sponsor&utm_campaign=instaloader
:height: 96px
# get followees
followees = instaloader.get_followees(PROFILE, session)
for f in followees:
print("%i\t%s\t%s" % (f['follower_count'], f['username'], f['full_name']))
.. current-sponsors-end
Then, you may download all pictures of all followees with
It is a pleasure for us to share our Instaloader to the world, and we are proud
to have attracted such an active and motivating community, with so many users
who share their suggestions and ideas with us. Buying a community-sponsored beer
or coffee from time to time is very likely to further raise our passion for the
development of Instaloader.
.. code:: python
| For Donations, we provide GitHub Sponsors page, a PayPal.Me link and a Bitcoin address.
| GitHub Sponsors: `Sponsor @aandergr on GitHub Sponsors <https://github.com/sponsors/aandergr>`__
| PayPal: `PayPal.me/aandergr <https://www.paypal.me/aandergr>`__
| BTC: 1Nst4LoadeYzrKjJ1DX9CpbLXBYE9RKLwY
for f in followees:
try:
instaloader.download(f['username'], session)
except instaloader.NonfatalException:
pass
Disclaimer
----------
You could also download your last 20 liked pics with
.. disclaimer-start
.. code:: python
Instaloader is in no way affiliated with, authorized, maintained or endorsed by Instagram or any of its affiliates or
subsidiaries. This is an independent and unofficial project. Use at your own risk.
instaloader.download_feed_pics(session, max_count=20, fast_update=True,
filter_func=lambda node: not node["likes"]["viewer_has_liked"])
Instaloader is licensed under an MIT license. Refer to ``LICENSE`` file for more information.
Each Instagram profile has its own unique ID which stays unmodified even
if a user changes his/her username. To get said ID, given the profile's
name, you may call
.. code:: python
instaloader.get_id_by_username(PROFILE_NAME)
``get_followees()`` also returns unique IDs for all loaded followees. To
get the current username of a profile, given this unique ID
``get_username_by_id()`` can be used. For example:
.. code:: python
instaloader.get_username_by_id(session, followees[0]['id'])
.. disclaimer-end
+19
View File
@@ -0,0 +1,19 @@
pkgbase = instaloader
pkgdesc = Command line tool to download pictures, videos and metadata from Instagram
pkgver = {{version}}
pkgrel = 1
url = https://instaloader.github.io/
arch = any
license = MIT
makedepends = python-build
makedepends = python-installer
makedepends = python-setuptools
makedepends = python-wheel
depends = python
depends = python-requests
optdepends = python-browser-cookie3: Import session cookies from browser
options = !emptydirs
source = instaloader-{{version}}.tar.gz::https://codeload.github.com/instaloader/instaloader/tar.gz/v{{version}}
sha512sums = {{hash}}
pkgname = instaloader
+37
View File
@@ -0,0 +1,37 @@
# Maintainer: André Koch-Kramer <koch-kramer@web.de>
pkgname=instaloader
pkgver={{version}}
pkgrel=1
pkgdesc="Command line tool to download pictures, videos and metadata from Instagram"
arch=('any')
url="https://instaloader.github.io/"
license=('MIT')
groups=()
depends=(
python
python-requests
)
makedepends=(
python-build
python-installer
python-setuptools
python-wheel
)
optdepends=(
'python-browser-cookie3: Import session cookies from browser'
)
options=('!emptydirs')
source=($pkgname-$pkgver.tar.gz::https://codeload.github.com/instaloader/instaloader/tar.gz/v$pkgver)
sha512sums=('{{hash}}')
build() {
cd "$srcdir/$pkgname-$pkgver"
python -m build --wheel --no-isolation
}
package() {
cd "$srcdir/$pkgname-$pkgver"
install -Dm644 LICENSE "$pkgdir"/usr/share/licenses/$pkgname/LICENSE
python -m installer --destdir="$pkgdir" dist/*.whl
}
+32
View File
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
VERSION=${1:1}
# do not deploy pre-releases
echo $VERSION | grep -qe "[abc]" && exit 0
cd $(dirname $0)
# decrypt and add ssh key
openssl aes-256-cbc -K $AUR_KEY -iv $AUR_IV -in id_rsa_AUR.enc -out /tmp/AUR_openssh -d
eval "$(ssh-agent -s)"
chmod 600 /tmp/AUR_openssh
ssh-add /tmp/AUR_openssh
mkdir -p ~/.ssh
ssh-keyscan -t rsa aur.archlinux.org >> ~/.ssh/known_hosts
# clone and modify AUR repo
git clone --depth 1 ssh://aur@aur.archlinux.org/instaloader.git
curl -sSfOJ https://codeload.github.com/instaloader/instaloader/tar.gz/$1
HASH=$(sha512sum instaloader-$VERSION.tar.gz | cut -f1 -d " ")
sed -e "s/{{version}}/$VERSION/g" -e "s/{{hash}}/$HASH/g" PKGBUILD.template > instaloader/PKGBUILD
sed -e "s/{{version}}/$VERSION/g" -e "s/{{hash}}/$HASH/g" .SRCINFO.template > instaloader/.SRCINFO
# commit and push changes
cd instaloader
git config user.email "koch-kramer@web.de"
git config user.name "André Koch-Kramer"
git add .
git commit -m "Release of version $VERSION"
git push
Binary file not shown.
+674
View File
@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+16
View File
@@ -0,0 +1,16 @@
instaloader.exe is a standalone executable built with PyInstaller. Besides Instaloader itself
(see ../LICENSE.txt), it contains the following third-party software:
- Python, its standard library and the libraries it is built with, see Python.txt and
Python-incorporated-software.txt.
- The Python packages listed in third-party.txt, along with their license texts.
browser-cookie3 is licensed under the GNU Lesser General Public License version 3, whose text is
included in third-party.txt. It refers to the GNU General Public License version 3, see GPL-3.0.txt.
certifi is licensed under the Mozilla Public License 2.0, whose text is included in third-party.txt.
Their source code, in the versions listed in third-party.txt, is available at
https://pypi.org/project/browser-cookie3/#history and https://pypi.org/project/certifi/#history.
You may build instaloader.exe with modified versions of these packages: the source code of
Instaloader, including the script used to build instaloader.exe (deploy/windows/create_exe.py),
is published along with each release at https://github.com/instaloader/instaloader/releases.
+90
View File
@@ -0,0 +1,90 @@
import hashlib
import json
import os
import platform
import re
import shutil
import subprocess
import sys
import urllib.request
# copy the required files into repo root
shutil.copy('docs/favicon.ico', '.')
shutil.copy('deploy/windows/instaloader.spec', '.')
code = """
import contextlib
import psutil
import subprocess
def __main():
with contextlib.suppress(AttributeError, psutil.Error):
if psutil.Process().parent().parent().name() == "explorer.exe":
subprocess.Popen("powershell -NoExit -Command \\\"& '{0}'\\\"".format(sys.argv[0]))
return
main()
if __name__ == "__main__":
__main()
"""
with open('instaloader/__main__.py', 'r') as f:
# adjust imports for changed file structure
regex = re.compile(r'from (?:(\.[^ ]+ )|\.( ))import')
lines = [regex.sub(r'from instaloader\1\2import', line) for line in f.readlines()]
# insert code for magic exe behavior
index = lines.index('if __name__ == "__main__":\n')
code_lines = [cl + '\n' for cl in code.splitlines()]
for i, code_line in enumerate(code_lines):
if i + index < len(lines):
lines[i + index] = code_line
else:
lines.extend(code_lines[i:])
break
with open('__main__.py', 'w+') as f:
f.writelines(lines)
# packages bundled into the executable, whose licenses have to be shipped along with it
with open('Pipfile.lock') as f:
locked_packages = json.load(f)['default']
bundled_packages = ' '.join(list(locked_packages) + ['psutil', 'pyinstaller'])
os.makedirs('dist/licenses', exist_ok=True)
# install dependencies, invoke PyInstaller and collect licenses
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",
f"pipenv run pip-licenses --packages {bundled_packages} --with-license-file "
"--with-notice-file --no-license-path --format=plain-vertical --output-file dist/licenses/third-party.txt"]
for command in commands:
print()
print('#' * (len(command) + 6))
print('## {} ##'.format(command))
print('#' * (len(command) + 6))
print(flush=True)
err = subprocess.Popen(command).wait()
if err != 0:
sys.exit(err)
# calculate and store MD5 hash for created executable
hash_md5 = hashlib.md5()
with open('dist/instaloader.exe', 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
hash_md5.update(chunk)
with open('dist/instaloader.exe.md5', 'w+') as f:
f.write('{} *instaloader.exe\n'.format(hash_md5.hexdigest()))
# include the remaining license texts
shutil.copy('LICENSE', 'dist/LICENSE.txt')
shutil.copy('deploy/windows/NOTICE.txt', 'dist/licenses/NOTICE.txt')
shutil.copy('deploy/windows/GPL-3.0.txt', 'dist/licenses/GPL-3.0.txt')
shutil.copy(os.path.join(sys.base_prefix, 'LICENSE.txt'), 'dist/licenses/Python.txt')
urllib.request.urlretrieve('https://raw.githubusercontent.com/python/cpython/v{}/Doc/license.rst'
.format(platform.python_version()), 'dist/licenses/Python-incorporated-software.txt')
# Create ZIP file
shutil.make_archive('instaloader-{}-windows-standalone'.format(os.getenv('VERSION_TAG')), 'zip', 'dist')
+28
View File
@@ -0,0 +1,28 @@
# -*- mode: python -*-
block_cipher = None
a = Analysis(['__main__.py'],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
runtime_hooks=[],
excludes=['setuptools', 'pkg_resources', '_distutils_hack', 'packaging'],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
name='instaloader',
debug=False,
strip=False,
upx=False,
runtime_tmpdir=None,
console=True , icon='favicon.ico')
+1
View File
@@ -0,0 +1 @@
_build/
+1
View File
@@ -0,0 +1 @@
../.sponsor-logos
+231
View File
@@ -0,0 +1,231 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " epub3 to make an epub3"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
@echo " dummy to check syntax errors of document sources"
.PHONY: clean
clean:
rm -rf $(BUILDDIR)/*
.PHONY: html
html:
LC_ALL=C $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
.PHONY: dirhtml
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
.PHONY: singlehtml
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
.PHONY: pickle
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
.PHONY: json
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
.PHONY: htmlhelp
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
.PHONY: qthelp
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Instaloader.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Instaloader.qhc"
.PHONY: applehelp
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
.PHONY: devhelp
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/Instaloader"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Instaloader"
@echo "# devhelp"
.PHONY: epub
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
.PHONY: epub3
epub3:
$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
@echo
@echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
.PHONY: latex
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
.PHONY: latexpdf
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: latexpdfja
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
.PHONY: text
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
.PHONY: man
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
.PHONY: texinfo
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
.PHONY: info
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
.PHONY: gettext
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
.PHONY: changes
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
.PHONY: linkcheck
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
.PHONY: doctest
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
.PHONY: coverage
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
.PHONY: xml
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
.PHONY: pseudoxml
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
.PHONY: dummy
dummy:
$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
@echo
@echo "Build finished. Dummy builder generates no files."
favicon.ico: logo.svg
convert -density 384 -background transparent -define icon:auto-resize -colors 256 logo.svg favicon.ico
logo.png: logo.svg
inkscape -z -e logo.png -w 100 -h 100 logo.svg
+16
View File
@@ -0,0 +1,16 @@
# Instaloader Documentation
This directory contains sources for the Instaloader documentation.
The documentation is hosted at:
https://instaloader.github.io/
## Build
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):
```
pipenv sync --dev
make -C docs html
```
File diff suppressed because one or more lines are too long
+50
View File
File diff suppressed because one or more lines are too long
+83
View File
@@ -0,0 +1,83 @@
{% extends "!layout.html" %}
{% set html5_doctype = True %}
{% set script_files = script_files + ['_static/instaloaderdoc.js'] %}
{% block htmltitle %}
{% if pagename == "index" %}
<title>{{ title|striptags|e }}</title>
{% else %}
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
{% endif %}
{% endblock %}
{% block extrahead %}
{{ super() }}
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="google-site-verification" content="FNUsDdkOJM9obm9QEdrNBVd9AtAzHzA-aaSMCUrS6C8"/>
{% if pagename == "index" %}
<link rel="canonical" href="https://instaloader.github.io/"/>
{% else %}
<link rel="canonical" href="https://instaloader.github.io/{{ pagename }}.html"/>
{% endif %}
{% endblock %}
{% block header %}
<nav class="navbar navbar-light navbar-expand bg-light border-bottom">
<a class="navbar-brand" href="{{ pathto(master_doc) }}">
<img src="{{ pathto('_static/logo.png', 1) }}" width="30" height="30" class="d-inline-block align-top" alt="logo">
<span class="ml-3">Instaloader</span>
</a>
<ul class="navbar-nav mr-auto">
<li class="nav-item ml-0 ml-sm-2">
<a class="nav-link"
href="https://github.com/instaloader/instaloader/releases/tag/v{{ current_release }}"
title="Released on {{ current_release_date }}" data-toggle="tooltip">
v<b>{{ current_release }}</b>
</a>
</li>
</ul>
<ul class="navbar-nav ml-auto d-none d-sm-flex">
<li class="nav-item">
<a class="nav-link" href="https://github.com/instaloader/instaloader/issues">
Issues
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/instaloader/instaloader/releases">
Releases
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/instaloader/instaloader">
GitHub
</a>
</li>
</ul>
<button class="d-inline-block d-md-none btn btn-outline-primary btn-sm" type="button"
data-toggle="collapse" data-target="#instaloaderdocSidebar" aria-controls="instaloaderdocSidebar"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
</nav>
{% endblock %}
{% block relbar1 %}{% endblock %}
{% block relbar2 %}{% endblock %}
{% block footer %}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="instaloaderdoc-sidebar collapse d-md-block col-md-3 col-xl-2 bg-light"
id="instaloaderdocSidebar">
<nav role="navigation" aria-label="Table of Contents">
<form action="{{ pathto('search') }}" method="get" role="search" class="border-bottom mr-2 pb-2 mb-2 px-1">
<label class="sr-only" for="searchQueryInput">Search</label>
<input type="search" name="q" id="searchQueryInput" class="form-control my-2"
placeholder="Search..." aria-label="Search">
</form>
<h4>Contents</h4>
{{ toctree(maxdepth=-1) }}
</nav>
</div>
<main class="col-12 col-md-9 col-xl-10 pt-2" role="main">
{% block body %}{% endblock %}
</main>
</div>
</div>
{% endblock %}
+10
View File
@@ -0,0 +1,10 @@
{% extends "!page.html" %}
{% block body %}
{{ super() }}
{% if next and prev %}
<h2>Next Section</h2>
<p>
<a class="reference internal" href="{{ next.link|e }}">{{ next.title }}</a>
</p>
{% endif %}
{% endblock %}
+28
View File
@@ -0,0 +1,28 @@
{% extends "!search.html" %}
{% block body %}
<div id="fallback" class="admonition warning">
<script type="text/javascript">$('#fallback').hide();</script>
<p>
{% trans %}Please activate JavaScript to enable the search
functionality.{% endtrans %}
</p>
</div>
{% if search_performed %}
<h1>{{ _('Search Results') }}</h1>
{% if not search_results %}
<p>{{ _('Your search did not match any documents. Please make sure that all words are spelled correctly and that you\'ve selected enough categories.') }}</p>
{% endif %}
{% endif %}
<div id="search-results">
{% if search_results %}
<ul>
{% for href, caption, context in search_results %}
<li><a href="{{ pathto(item.href) }}">{{ caption }}</a>
<div class="context">{{ context|e }}</div>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endblock %}
+107
View File
@@ -0,0 +1,107 @@
.. meta::
:description:
Documentation of Instaloader module, a powerful and intuitive
Python library to download Instagram media and metadata.
.. _python-module-instaloader:
Python Module :mod:`instaloader`
--------------------------------
.. module:: instaloader
.. highlight:: python
Instaloader exposes its internally used methods and structures as a Python
module, making it a powerful and intuitive Python API for Instagram,
allowing to further customize obtaining media and metadata.
Start with getting an instance of :class:`Instaloader`::
import instaloader
# Get instance
L = instaloader.Instaloader()
# Optionally, login or load session
L.login(USER, PASSWORD) # (login)
L.interactive_login(USER) # (ask password on terminal)
L.load_session_from_file(USER) # (load session created w/
# `instaloader -l USERNAME`)
:mod:`instaloader` provides the :class:`Post` structure, which represents a
picture, video or sidecar (set of multiple pictures/videos) posted in a user's
profile. :class:`Instaloader` provides methods to iterate over Posts from a
certain source::
for post in instaloader.Hashtag.from_name(L.context, 'cat').get_posts():
# post is an instance of instaloader.Post
L.download_post(post, target='#cat')
:class:`Post` instances can be created with:
- :func:`Post.from_shortcode`
Use a Post shortcode (part of the Post URL,
``https://www.instagram.com/p/SHORTCODE/``) to create a Post
object::
post = Post.from_shortcode(L.context, SHORTCODE)
- :meth:`Profile.get_posts`
All media of a :class:`Profile`.
- :meth:`Profile.get_saved_posts`
Media that the user marked as saved (:class:`Profile` must be own profile
for this to work)
- :meth:`Instaloader.get_feed_posts`
Media in the user's feed.
- :meth:`Instaloader.get_explore_posts`
Media that is suggested by Instagram to explore.
- :meth:`Hashtag.get_posts`
Media associated with given hashtag.
With the :class:`Profile` class, Instaloader also makes it easy to access
metadata of a Profile. :class:`Profile` instances can be created with:
- :meth:`Profile.from_username`::
profile = Profile.from_username(L.context, USERNAME)
- :meth:`Profile.from_id`
given its User ID. This allows to easily lookup a Profile's username given
its ID::
Profile.from_id(L.context, USERID).username
- :meth:`Profile.get_followees`
Profiles that are followed by given user.
- :meth:`Profile.get_followers`
Profiles that follow given user.
- :attr:`Post.owner_profile`, :attr:`Story.owner_profile`, :attr:`StoryItem.owner_profile`
Owner profile of particular object.
- :meth:`Post.get_likes`
Profiles that liked a given :class:`Post`
- :attr:`PostComment.owner`
Profile of a Post comment.
For a list of a few code examples that use the Instaloader module, see
:ref:`codesnippets`.
The reference of the classes and functions provided by the :mod:`instaloader` module is
divided into the following subsections:
.. toctree::
:maxdepth: 2
module/instaloader
module/structures
module/nodeiterator
module/instaloadercontext
module/exceptions
+387
View File
@@ -0,0 +1,387 @@
.. meta::
:description:
How to download pictures from Instagram. Description of basic
usage of Instaloader, free tool to download photos from public
and private profiles, hashtags, stories, feeds, saved media, and
their metadata, comments and captions.
.. _download-pictures-from-instagram:
Download Pictures from Instagram
---------------------------------
.. highlight:: none
Here we describe how to use Instaloader to download pictures from Instagram. If
you do not have Instaloader installed yet, see :ref:`install`.
.. NOTE that Section "Basic Usage" is duplicated in README.rst.
Basic Usage
^^^^^^^^^^^
To **download all pictures and videos of a profile**, as well as the
**profile picture**, do
::
instaloader profile [profile ...]
where ``profile`` is the name of a profile you want to download. Instead
of only one profile, you may also specify a list of profiles.
To later **update your local copy** of that profiles, you may run
::
instaloader --fast-update profile [profile ...]
If :option:`--fast-update` is given, Instaloader stops when arriving at the
first already-downloaded picture.
Alternatively, you can use :option:`--latest-stamps` to have Instaloader store
the time each profile was last downloaded and only download newer media:
::
instaloader --latest-stamps -- profile [profile ...]
With this option it's possible to move or delete downloaded media and still keep
the archive updated.
When updating profiles, Instaloader
automatically **detects profile name changes** and renames the target directory
accordingly.
Instaloader can also be used to **download private profiles**. To do so,
invoke it with
::
instaloader --login=your_username profile [profile ...]
When logging in, Instaloader **stores the session cookies** in a file in your
home directory, which will be reused later the next time :option:`--login`
is given. So you can download private profiles **non-interactively** when you
already have a valid session cookie file.
.. _what-to-download:
What to Download
^^^^^^^^^^^^^^^^
.. targets-start
Instaloader supports the following targets:
- ``profile``
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.
Besides the profile's posts, its current profile picture is downloaded. For
each profile you download,
- :option:`--stories`
instructs Instaloader to also **download the user's stories**,
- :option:`--highlights`
to **download the highlights of that profile**,
- :option:`--tagged`
to **download posts where the user is tagged**, and
- :option:`--reels`
to **download Reels videos**.
- :option:`--igtv`
to **download IGTV videos**.
- ``"#hashtag"``
Posts with a certain **hashtag** (the quotes are usually necessary). Requires :ref:`login<login>`.
- ``%location id``
Posts tagged with a given location; the location ID is the numerical ID
Instagram labels a location with (e.g.
\https://www.instagram.com/explore/locations/**362629379**/plymouth-naval-memorial/).
Requires :ref:`login<login>`.
.. versionadded:: 4.2
- ``:stories``
The currently-visible **stories** of your followees (requires
:ref:`login<login>`).
- ``:feed``
Your **feed** (requires :ref:`login<login>`).
- ``:saved``
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 :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. For example, to download the post
https://www.instagram.com/p/**B_K4CykAOtf**, run the command::
instaloader -- -B_K4CykAOtf
.. versionadded:: 4.1
.. targets-end
Instaloader goes through all media matching the specified targets and
downloads the pictures and videos and their captions. You can specify
- :option:`--comments`
also **download comments** of each post,
- :option:`--geotags`
**download geotags** of each post and save them as
Google Maps link (requires :ref:`login<login>`),
For a reference of all supported command line options, see
:ref:`command-line-options`.
.. _filename-specification:
Filename Specification
^^^^^^^^^^^^^^^^^^^^^^
For each target, Instaloader creates a directory named after the target,
i.e. ``profile``, ``#hashtag``, ``%location id``, ``:feed``, etc. and therein saves the
posts in files named after the post's timestamp.
:option:`--dirname-pattern` allows to configure the directory name of each
target. The default is ``--dirname-pattern={target}``. In the dirname
pattern, the token ``{target}`` is replaced by the target name, and
``{profile}`` is replaced by the owner of the post which is downloaded.
:option:`--filename-pattern` configures the path of the post and story's files relative
to the target directory that is specified with :option:`--dirname-pattern`.
The default is ``--filename-pattern={date_utc}_UTC``.
The tokens ``{target}`` and ``{profile}`` are replaced like in the
dirname pattern.
:option:`--title-pattern` is similar to :option:`--filename-pattern`, but for profile
pics, hashtag profile pics, and highlight covers. The default is
``{date_utc}_UTC_{typename}`` if :option:`--dirname-pattern` contains ``{target}`` or
``{profile}``, or ``{target}_{date_utc}_UTC_{typename}`` if it does not. Some tokens
are not supported for this option, see below for details.
The following tokens are defined for usage with
:option:`--filename-pattern` and :option:`--title-pattern`:
- ``{target}``
Target name (as given in Instaloader command line)
- ``{profile}`` (same as ``{owner_username}``)
Owner of the Post / StoryItem / ProfilePic. For hashtag profile pics and
highlight covers, equivalent to ``{target}``.
- ``{owner_id}``
Unique integer ID of owner profile. For hashtag profile pics, equivalent to
``{target}``.
- ``{shortcode}``
Shortcode (identifier string). Not available for :option:`--title-pattern`.
- ``{mediaid}``
Integer representation of shortcode. Not available for :option:`--title-pattern`.
- ``{filename}``
Instagram's internal filename.
- ``{date_utc}`` (same as ``{date}``)
Creation time in UTC timezone.
`strftime()-style formatting options <https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior>`__
are supported as format specifier. The default date format specifier used by
Instaloader is::
{date_utc:%Y-%m-%d_%H-%M-%S}
- ``{typename}``
Type of media being saved, such as GraphImage, GraphStoryVideo, profile_pic,
etc.
For example, encode the poster's profile name in the filenames with::
instaloader --filename-pattern={date_utc}_UTC_{profile} "#hashtag"
As another example, you may instruct Instaloader to store posts in a
``PROFILE/YEAR/SHORTCODE.jpg`` directory structure::
instaloader --dirname-pattern={profile} --filename-pattern={date_utc:%Y}/{shortcode} <target> ...
.. _filter-posts:
Filter Posts
^^^^^^^^^^^^
.. py:currentmodule:: instaloader
The options :option:`--post-filter` and :option:`--storyitem-filter`
allow to specify criteria that posts or story items have to
meet to be downloaded. If not given, all posts are downloaded.
The filter string must be a
`Python boolean expression <https://docs.python.org/3/reference/expressions.html#boolean-operations>`__
where the attributes from :class:`Post` or
:class:`StoryItem` respectively are defined.
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)
Owner profile username / user ID.
- :attr:`~Post.date_utc` (datetime), :attr:`~Post.date_local` (datetime)
Creation timestamp. Since :class:`~datetime.datetime` objects can be created
inside filter strings, this easily allows filtering by creation date. E.g.::
instaloader --post-filter="date_utc <= datetime(2018, 5, 31)" target
- :attr:`~Post.is_video` (bool)
Whether Post/StoryItem is a video. For example, you may skip videos::
instaloader --post-filter="not is_video" target
This is not the same as :option:`--no-videos` and
:option:`--no-video-thumbnails`, since sidecar posts (posts that contain
multiple pictures/videos in one post) have this attribute set to False.
As :option:`--post-filter`, the following attributes can be used additionally:
- :attr:`~Post.viewer_has_liked` (bool)
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
- :attr:`~Post.likes` (int), :attr:`~Post.comments` (int)
Likes count / comments count. You might only want to download posts that
were either liked by yourself or by many others::
instaloader --login=your_username --post-filter="likes>100 or viewer_has_liked" profile
- :attr:`~Post.caption_hashtags` (list of str) / :attr:`~Post.caption_mentions` (list of str)
``#hashtags`` or ``@mentions`` (lowercased) in the Post's caption. For example, to
download posts of kittens that are cute::
instaloader --post-filter="'cute' in caption_hashtags" "#kitten"
- :attr:`~Post.tagged_users` (list of str)
Lowercased usernames that are tagged in the Post.
For :option:`--storyitem-filter`, the following additional attributes are
defined:
- :attr:`~StoryItem.expiring_utc` (datetime) / :attr:`~StoryItem.expiring_local` (datetime)
Timestamp when StoryItem will get unavailable.
.. _metadata-text-files:
Metadata Text Files
^^^^^^^^^^^^^^^^^^^
Unless :option:`--no-captions` is given, Instaloader creates a ``.txt`` file
along with each post where the Post's caption is saved.
You can customize what metadata to save for each Post or StoryItem with
:option:`--post-metadata-txt` and :option:`--storyitem-metadata-txt`. The
default is ``--post-metadata-txt={caption}`` and no storyitem metadata txt.
These strings are formatted similar as the path patterns described in :ref:`filename-specification` and
the result is saved in text files, unless it is empty.
Specifying these options multiple times results in output having multiple lines,
in the order they were given to Instaloader.
The field names are evaluated to :class:`Post` or :class:`StoryItem` attributes,
and as such, the same fields are supported as in :ref:`filename-specification`
and :ref:`filter-posts`.
For example, to save the current number of likes for each post, rather than
the post's caption::
instaloader --post-metadata-txt="{likes} likes." <target>
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
^^^^^^^^^^^^^^^^^^^^^^
Instaloader is suitable for running as a cronjob to periodically update your
personal Instagram archive. The :option:`--quiet` option disables user
interactions and logging of non-error messages. To non-interactively use
Instaloader logged-in, create a session file::
instaloader --login=your_username
Then use the same username in your cronjob to load the session and download
the given targets::
instaloader --login=your_username --quiet target [...]
Instaloader saves the session file to
``~/.config/instaloader/session-YOUR-USERNAME``. See
:option:`--sessionfile` option for how to override this path.
Programming Instaloader
^^^^^^^^^^^^^^^^^^^^^^^
If your task cannot be done with the command line interface of Instaloader,
consider taking a look at the :ref:`python-module-instaloader`.
Instaloader exposes its internally used methods and structures, making it a
powerful and intuitive Python API for Instagram, allowing to further customize
obtaining media and metadata.
Also see :ref:`codesnippets`, where we collect a few example scripts that use
Instaloader for simple tasks that cannot be done with the command line
interface.
+377
View File
@@ -0,0 +1,377 @@
.. _command-line-options:
Command Line Options
====================
Instaloader is invoked with::
$ instaloader [options] target [target ...]
where ``target`` is a ``profile``, a ``"#hashtag"``, ``@profile`` (all profiles
that *profile* is following), ``%location ID``, or if logged in ``:feed`` (pictures from your
feed), ``:stories`` (stories of your followees) or ``:saved`` (collection of
posts marked as saved).
Here we explain the additional options that can be given to Instaloader to
customize its behavior. For an
introduction on how to use Instaloader, see
:ref:`download-pictures-from-instagram`.
To get a list of all flags, their abbreviations and
their descriptions, you may also run::
instaloader --help
Targets
^^^^^^^
Specify a list of targets. For each of these, Instaloader creates a folder and
stores all posts along with the pictures' captions there.
.. include:: basic-usage.rst
:start-after: targets-start
:end-before: targets-end
- ``filename.json[.xz]``
Re-Download the given object
- ``+args.txt``
Read targets (and options) from given text file. See :option:`+args.txt`.
What to Download of each Post
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. option:: --no-pictures
Do not download post pictures. Cannot be used together with
:option:`--fast-update`. Implies :option:`--no-video-thumbnails`, does not
imply :option:`--no-videos`.
.. versionadded:: 4.1
.. option:: --no-videos, -V
Do not download videos.
.. option:: --no-video-thumbnails
Do not download thumbnails of videos.
.. option:: --geotags, -G
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 :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. Requires :ref:`login<login>`.
.. option:: --no-captions
Do not create txt files.
.. option:: --post-metadata-txt
Template to write in txt file for each Post. See :ref:`metadata-text-files`.
.. option:: --storyitem-metadata-txt
Template to write in txt file for each StoryItem. See
:ref:`metadata-text-files`.
.. option:: --slide
Download only selected images of a sidecar. You can select single images using their
index in the sidecar starting with the leftmost or you can specify a range of images
with the following syntax: ``start_index-end_index``. Example:
``--slide 1`` will select only the first image, ``--slide last`` only the last one and ``--slide 1-3`` will select only
the first three images.
.. versionadded:: 4.6
.. option:: --no-metadata-json
Do not create a JSON file containing the metadata of each post.
.. option:: --no-compress-json
Do not xz compress JSON files, rather create pretty formatted JSONs.
What to Download of each Profile
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. option:: --no-posts
Do not download regular posts.
.. versionadded:: 4.1
.. option:: --no-profile-pic
Do not download profile picture.
.. option:: --stories, -s
Also download stories of each profile that is downloaded. Requires
:ref:`login<login>`.
.. option:: --highlights
Also download highlights of each profile that is downloaded. Requires
:ref:`login<login>`.
.. versionadded:: 4.1
.. option:: --tagged
Also download posts where each profile is tagged.
.. versionadded:: 4.1
.. option:: --reels
Also download Reels videos.
.. versionadded:: 4.14
.. option:: --igtv
Also download IGTV videos.
.. versionadded:: 4.3
Which Posts to Download
^^^^^^^^^^^^^^^^^^^^^^^
.. option:: --fast-update, -F
For each target, stop when encountering the first already-downloaded picture.
This flag is recommended when you use Instaloader to update your personal
Instagram archive.
.. option:: --latest-stamps [STAMPSFILE]
Works similarly to :option:`--fast-update`, but instead of relying on already
downloaded media, the time each profile was downloaded is stored, and only
media newer than the last download is fetched. This allows updating your
personal Instagram archive while emptying the target directories.
Only works for media associated with a specific profile, and that is returned
in chronological order: profile posts, profile stories, profile IGTV posts
and profile tagged posts.
By default, the information is stored in
``~/.config/instaloader/latest-stamps.ini``, but you can specify an
alternative location.
.. versionadded:: 4.8
.. option:: --post-filter filter, --only-if filter
Expression that, if given, must evaluate to True for each post to be
downloaded. Must be a syntactically valid Python expression. Variables are
evaluated to :class:`instaloader.Post` attributes. Example:
``--post-filter=viewer_has_liked``. See :ref:`filter-posts` for more
examples.
.. option:: --storyitem-filter filter
Expression that, if given, must evaluate to True for each storyitem to be
downloaded. Must be a syntactically valid Python expression. Variables are
evaluated to :class:`instaloader.StoryItem` attributes.
See :ref:`filter-posts` for more examples.
.. option:: --count COUNT, -c
Do not attempt to download more than COUNT posts. Applies to
``#hashtag``, ``%location_id``, ``:feed``, and ``:saved``.
.. _login:
Login (Download Private Profiles)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Instaloader can login to Instagram. This allows downloading private
profiles. To login, pass the :option:`--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.
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
Load Instagram session cookies from a browser instead of using Instaloader's own login mechanism.
This method requires the `browser_cookie3 <https://github.com/borisbabic/browser_cookie3>`__ library.
By default, cookies are loaded from the browser's main profile, but you can specify a custom cookie file using :option:`--cookiefile`.
Since :option:`--load-cookies` and :option:`--login` are alternative ways to acquire a session, they cannot be used together when initially acquiring a session.
Supported browsers: Arc, Brave, Chrome, Chromium, Edge, Firefox, LibreWolf, Opera, Opera_GX, Safari, and Vivaldi.
Once logged in via browser cookies, Instaloader saves the session, allowing you to use :option:`--login` in subsequent runs.
.. versionadded:: 4.11
.. option:: --cookiefile COOKIE-FILE, -B COOKIE-FILE
Specifies a cookie file path for :option:`--load-cookies` to load cookies from a particular browser profile.
This option must be used together with :option:`--load-cookies` and does not work on its own.
.. versionadded:: 4.11
.. option:: --sessionfile SESSIONFILE, -f SESSIONFILE
Path for loading and storing session key file. Defaults to
``~/.config/instaloader/session-YOUR-USERNAME``.
.. option:: --password YOUR-PASSWORD, -p YOUR-PASSWORD
Password for your Instagram account. Without this option, you'll be prompted
for your password interactively if there is not yet a valid session file.
.. warning:: Using :option:`--password` option is discouraged for security
reasons. Enter your password interactively when asked, or use the
sessionfile feature (:option:`--sessionfile` to customize path).
How to Download
^^^^^^^^^^^^^^^
.. option:: --dirname-pattern DIRNAME_PATTERN
Name of directory where to store posts. ``{profile}`` is replaced by the
profile name, ``{target}`` is replaced by the target you specified, i.e.
either ``:feed``, ``#hashtag`` or the profile name. Defaults to ``{target}``.
See :ref:`filename-specification`.
.. option:: --filename-pattern FILENAME_PATTERN
Prefix of filenames for posts and stories, relative to the directory given with
:option:`--dirname-pattern`. ``{profile}`` is replaced by the profile name,
``{target}`` is replaced by the target you specified, i.e. either ``:feed``,
``#hashtag`` or the profile name. Defaults to ``{date_utc}_UTC``.
See :ref:`filename-specification` for a list of supported tokens.
.. option:: --title-pattern TITLE_PATTERN
Prefix of filenames for profile pics, hashtag profile pics, and highlight
covers, relative to the directory given with :option:`--dirname-pattern`.
Defaults to ``{date_utc}_UTC_{typename}`` if :option:`--dirname-pattern`
contains ``{target}`` or ``{profile}``, otherwise defaults to
``{target}_{date_utc}_UTC_{typename}``.
See :ref:`filename-specification` for a list of supported tokens.
.. versionadded:: 4.8
.. option:: --sanitize-paths
Force sanitization of paths so that the resulting file and directory names
are valid on both Windows and Unix.
.. versionadded:: 4.9
.. option:: --resume-prefix prefix
For many targets, Instaloader is capable of resuming a previously-aborted
download loop. To do so, it creates a JSON file within the target directory
when interrupted. This option controls the prefix for filenames that are
used to save the information to resume an interrupted download. The default
prefix is ``iterator``.
Resuming an interrupted download is supported for the following targets:
- Profile posts,
- Profile IGTV posts (:option:`--igtv`),
- Profile tagged posts (:option:`--tagged`),
- Saved posts (``:saved``),
- Hashtags.
This feature is enabled by default for targets where it is supported;
:option:`--resume-prefix` only changes the name of the iterator files.
To turn this feature off, use :option:`--no-resume`.
JSON files with resume information are always compressed, regardless of
:option:`--no-compress-json`.
.. versionadded:: 4.5
.. option:: --no-resume
Do not resume a previously-aborted download iteration, and do not save such
information when interrupted.
.. versionadded:: 4.5
.. option:: --user-agent USER_AGENT
User Agent to use for HTTP requests. Per default, Instaloader pretends being
Chrome/142 on Linux.
.. option:: --max-connection-attempts N
Maximum number of connection attempts until a request is aborted. Defaults
to ``3``. If a connection fails, it can be manually skipped by hitting
:kbd:`Control-c`. Set this to ``0`` to retry infinitely.
.. option:: --request-timeout N
Seconds to wait before timing out a connection request. Defaults to 300.
.. versionadded:: 4.3
.. versionchanged:: 4.6
Enabled this option by default with a timeout of 300 seconds.
.. option:: --abort-on STATUS_CODE_LIST
Comma-separated list of HTTP status codes that cause Instaloader to abort,
bypassing all retry logic.
For example, with ``--abort-on=302,400,429``, Instaloader will stop if a
request is responded with a 302 redirect, a Bad Request error, or a Too Many
Requests error.
.. versionadded:: 4.7
.. option:: --no-iphone
Do not attempt to download iPhone version of images and videos.
.. versionadded:: 4.8
Miscellaneous Options
^^^^^^^^^^^^^^^^^^^^^
.. option:: --quiet, -q
Disable user interaction, i.e. do not print messages (except errors) and fail
if login credentials are needed but not given.
This is handy for running :ref:`instaloader-as-cronjob`.
.. option:: +args.txt
Read arguments from file `args.txt`, a shortcut to provide arguments from
file rather than command-line. This provides a convenient way to hide login
info from CLI, and can also be used to simplify management of long arguments.
You can provide more than one file at once, e.g.: ``+args1.txt +args2.txt``.
.. note::
Text file should separate arguments with line breaks.
`args.txt` example::
--login=MYUSERNAME
--password=MYPASSWORD
--fast-update
profile1
profile2
.. versionadded:: 4.1
+124
View File
@@ -0,0 +1,124 @@
.. _codesnippets:
Advanced Instaloader Examples
=============================
.. currentmodule:: instaloader
.. highlight:: python
.. contents::
:backlinks: none
Here we present code examples that use the :ref:`python-module-instaloader` for
more advanced Instagram downloading or metadata mining than what is possible
with the Instaloader command line interface.
The scripts presented here can be downloaded from our source tree:
`instaloader/docs/codesnippets/ <https://github.com/instaloader/instaloader/tree/master/docs/codesnippets>`__
.. For each code snippet:
- title
- brief description of what it does / motivation / how it works
- code, or link to code
- link to discussion issue
- link used methods
Download Posts in a Specific Period
-----------------------------------
To only download Instagram pictures (and metadata) that are within a specific
period, you can simply use :func:`~itertools.dropwhile` and
:func:`~itertools.takewhile` from :mod:`itertools` on a generator that returns
Posts in **exact chronological order**, such as :meth:`Profile.get_posts`.
.. literalinclude:: codesnippets/121_since_until.py
See also :class:`Post`, :meth:`Instaloader.download_post`.
Discussed in :issue:`121`.
The code example with :func:`~itertools.dropwhile` and
:func:`~itertools.takewhile` makes the assumption that the post iterator returns
posts in exact chronological order. As discussed in :issue:`666`, the following
approach fits for an **almost chronological order**, where up to *k* older posts
are inserted into an otherwise chronological order, such as a Hashtag feed.
.. literalinclude:: codesnippets/666_historical_hashtag_data.py
Likes of a Profile / Ghost Followers
------------------------------------
To obtain a list of your inactive followers, i.e. followers that did not like
any of your pictures, you can use this approach.
.. literalinclude:: codesnippets/120_ghost_followers.py
See also :meth:`Profile.get_posts`, :meth:`Post.get_likes`,
:meth:`Profile.get_followers`, :meth:`Instaloader.load_session_from_file`,
:meth:`Profile.from_username`.
Discussed in :issue:`120`.
Track Deleted Posts
-------------------
This script uses Instaloader to obtain a list of currently-online Instagram and
compares it with the set of posts that you already have downloaded. It outputs
a list of posts which are online but not offline (i.e. not yet downloaded) and a
list of posts which are offline but not online (i.e. deleted in the profile).
.. literalinclude:: codesnippets/56_track_deleted.py
See also :func:`load_structure_from_file`, :meth:`Profile.from_username`,
:meth:`Profile.get_posts`, :class:`Post`.
Discussed in :issue:`56`.
Only one Post per User
----------------------
To download only the single most recent post per user within a hashtag feed,
this snippet uses a :class:`set` that contains the users of whom a post has
already been downloaded. For each post, it checks whether the post's creator is
already contained in that set. If not, the post is downloaded from Instagram and
the user is added to that set.
.. literalinclude:: codesnippets/113_only_one_per_user.py
See also :class:`Post`, :meth:`Instaloader.download_post`,
:attr:`Post.owner_profile`, :class:`Profile`.
Discussed in :issue:`113`.
Top X Posts of User
-------------------
With Instaloader, it is easy to download the few most-liked pictures of a user.
.. literalinclude:: codesnippets/194_top_x_of_user.py
Discussed in :issue:`194`.
Metadata JSON Files
-------------------
The JSON files Instaloader saves along with each Post contain all the metadata
that has been retrieved from Instagram while downloading the picture and
associated required information.
With `jq <https://stedolan.github.io/jq/>`__, a command-line JSON processor, the
metadata can be easily post-processed. For example, Instaloader's JSON files can
be pretty-formatted with:
.. code-block:: none
xzcat 2018-05-13_11-18-45_UTC.json.xz | jq .node
However, Instaloader tries to do as few metadata requests as possible, so,
depending on how Instaloader has been invoked, it may occur that these files do
not contain the complete available metadata structure. Nevertheless, the file
can be loaded into Instaloader with :func:`load_structure_from_file` and the
required metadata then be accessed via the :class:`Post` or :class:`Profile`
attributes, which trigger an Instagram request if that particular information is
not present in the JSON file.
@@ -0,0 +1,14 @@
import instaloader
L = instaloader.Instaloader()
posts = instaloader.Hashtag.from_name(L.context, 'urbanphotography').get_posts_resumable()
users = set()
for post in posts:
if not post.owner_profile in users:
L.download_post(post, '#urbanphotography')
users.add(post.owner_profile)
else:
print("{} from {} skipped.".format(post, post.owner_profile))
+27
View File
@@ -0,0 +1,27 @@
import instaloader
L = instaloader.Instaloader()
USER = "your_account"
PROFILE = USER
# Load session previously saved with `instaloader -l USERNAME`:
L.load_session_from_file(USER)
profile = instaloader.Profile.from_username(L.context, PROFILE)
likes = set()
print("Fetching likes of all posts of profile {}.".format(profile.username))
for post in profile.get_posts():
print(post)
likes = likes | set(post.get_likes())
print("Fetching followers of profile {}.".format(profile.username))
followers = set(profile.get_followers())
ghosts = followers - likes
print("Storing ghosts into file.")
with open("inactive-users.txt", 'w') as f:
for ghost in ghosts:
print(ghost.username, file=f)
+15
View File
@@ -0,0 +1,15 @@
from datetime import datetime
from itertools import dropwhile, takewhile
import instaloader
L = instaloader.Instaloader()
posts = instaloader.Profile.from_username(L.context, "instagram").get_posts()
SINCE = datetime(2015, 5, 1)
UNTIL = datetime(2015, 3, 1)
for post in takewhile(lambda p: p.date > UNTIL, dropwhile(lambda p: p.date > SINCE, posts)):
print(post.date)
L.download_post(post, "instagram")
+17
View File
@@ -0,0 +1,17 @@
from itertools import islice
from math import ceil
from instaloader import Instaloader, Profile
PROFILE = ... # profile to download from
X_percentage = 10 # percentage of posts that should be downloaded
L = Instaloader()
profile = Profile.from_username(L.context, PROFILE)
posts_sorted_by_likes = sorted(profile.get_posts(),
key=lambda p: p.likes + p.comments,
reverse=True)
for post in islice(posts_sorted_by_likes, ceil(profile.mediacount * X_percentage / 100)):
L.download_post(post, PROFILE)
+34
View File
@@ -0,0 +1,34 @@
from glob import glob
from sys import argv
from os import chdir
from instaloader import Instaloader, Post, Profile, load_structure_from_file
# Instaloader instantiation - you may pass additional arguments to the constructor here
L = Instaloader()
# If desired, load session previously saved with `instaloader -l USERNAME`:
#L.load_session_from_file(USERNAME)
try:
TARGET = argv[1]
except IndexError:
raise SystemExit("Pass profile name as argument!")
# Obtain set of posts that are on hard disk
chdir(TARGET)
offline_posts = set(filter(lambda s: isinstance(s, Post),
(load_structure_from_file(L.context, file)
for file in (glob('*.json.xz') + glob('*.json')))))
# Obtain set of posts that are currently online
post_iterator = Profile.from_username(L.context, TARGET).get_posts()
online_posts = set(post_iterator)
if online_posts - offline_posts:
print("Not yet downloaded posts:")
print(" ".join(str(p) for p in (online_posts - offline_posts)))
if offline_posts - online_posts:
print("Deleted posts:")
print(" ".join(str(p) for p in (offline_posts - online_posts)))
@@ -0,0 +1,58 @@
from argparse import ArgumentParser
from glob import glob
from os.path import expanduser
from platform import system
from sqlite3 import OperationalError, connect
try:
from instaloader import ConnectionException, Instaloader
except ModuleNotFoundError:
raise SystemExit("Instaloader not found.\n pip install [--user] instaloader")
def get_cookiefile():
default_cookiefile = {
"Windows": "~/AppData/Roaming/Mozilla/Firefox/Profiles/*/cookies.sqlite",
"Darwin": "~/Library/Application Support/Firefox/Profiles/*/cookies.sqlite",
}.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]
def import_session(cookiefile, sessionfile):
print("Using cookies from {}.".format(cookiefile))
conn = connect(f"file:{cookiefile}?immutable=1", uri=True)
try:
cookie_data = conn.execute(
"SELECT name, value FROM moz_cookies WHERE baseDomain='instagram.com'"
)
except OperationalError:
cookie_data = conn.execute(
"SELECT name, value FROM moz_cookies WHERE host LIKE '%instagram.com'"
)
instaloader = Instaloader(max_connection_attempts=1)
instaloader.context._session.cookies.update(cookie_data)
username = instaloader.test_login()
if not username:
raise SystemExit("Not logged in. Are you logged in successfully in Firefox?")
print("Imported session cookie for {}.".format(username))
instaloader.context.username = username
instaloader.save_session_to_file(sessionfile)
if __name__ == "__main__":
p = ArgumentParser()
p.add_argument("-c", "--cookiefile")
p.add_argument("-f", "--sessionfile")
args = p.parse_args()
try:
import_session(args.cookiefile or get_cookiefile(), args.sessionfile)
except (ConnectionException, OperationalError) as e:
raise SystemExit("Cookie import failed: {}".format(e))
@@ -0,0 +1,31 @@
from datetime import datetime
import instaloader
L = instaloader.Instaloader()
posts = instaloader.Hashtag.from_name(L.context, "urbanphotography").get_posts_resumable()
SINCE = datetime(2020, 5, 10) # further from today, inclusive
UNTIL = datetime(2020, 5, 11) # closer to today, not inclusive
k = 0 # initiate k
#k_list = [] # uncomment this to tune k
for post in posts:
postdate = post.date
if postdate > UNTIL:
continue
elif postdate <= SINCE:
k += 1
if k == 50:
break
else:
continue
else:
L.download_post(post, "#urbanphotography")
# if you want to tune k, uncomment below to get your k max
#k_list.append(k)
k = 0 # set k to 0
#max(k_list)
+4
View File
@@ -0,0 +1,4 @@
# Advanced Instaloader Examples
This directory contains the scripts that are presented at:
https://instaloader.github.io/codesnippets.html
+88
View File
@@ -0,0 +1,88 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
import os
import platform
import subprocess
import sys
sys.path.insert(0, os.path.abspath('..'))
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'Instaloader'
copyright = '2023, Alexander Graf and Andre Koch-Kramer'
author = 'Alexander Graf and Andre Koch-Kramer'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.extlinks",
"sphinx.ext.githubpages",
"sphinx.ext.intersphinx",
"sphinxcontrib.jquery",
]
# autodoc options
autodoc_default_options = {
'show-inheritance': True,
'members': True,
'undoc-members': True,
}
# extlinks options
extlinks = {
'issue': (
'https://github.com/instaloader/instaloader/issues/%s',
'Issue #%s'
),
'example': (
'https://raw.githubusercontent.com/instaloader/instaloader/master/docs/codesnippets/%s',
'Example %s'
),
}
# intersphinx options
intersphinx_mapping = {
'python': ('https://docs.python.org/3', None),
'requests': ('https://requests.readthedocs.io/en/latest/', None),
}
# other options
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# ignore TypeVar as it is currently unsupported by Sphinx
nitpick_ignore = [('py:class', 'instaloader.nodeiterator.T')]
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
pygments_style = 'fruity'
html_theme = 'basic'
html_static_path = ['_static']
html_logo = "logo.png"
html_css_files = ['instaloaderdoc.css']
html_js_files = ['instaloaderdoc.js']
# get release number
current_release = subprocess.check_output(["git", "describe", "--abbrev=0"]).decode("ascii")[1:-1]
date_format = "%e %b %Y" if platform.system() != "Windows" else "%d %b %Y"
current_release_date = subprocess.check_output(
["git", "log", "-1", "--tags", "--format=%ad", "--date=format:"+date_format]).decode("ascii")[:-1]
html_context = {'current_release': current_release, 'current_release_date': current_release_date}
+148
View File
@@ -0,0 +1,148 @@
.. _contributing:
Contributing to Instaloader
===========================
As an open source project, Instaloader heavily depends on the contributions from
its community. In this document, we advise on how you may help Instaloader to
become an even greater tool.
Instaloader's development is organized on
`GitHub <https://github.com/instaloader/instaloader>`__, where Issues and Pull
Requests are discussed.
Contributing to an open source project is an unmatchable opportunity to improve
and manifest programming skills. For issues that are good starting points to get
involved in Instaloader development, as they do not require deep insight into
Instaloader's code base, have a look at our
`Issues labelled 'good first issue' <https://github.com/instaloader/instaloader/contribute>`__.
This document covers several ways of how you can contribute back to Instaloader.
Answering Questions
-------------------
The easiest way to help out is to answer questions. If you are interested in
answering questions regarding Instaloader, good places to begin are
- `Questions tagged 'instaloader' on Stack Overflow <https://stackoverflow.com/questions/tagged/instaloader>`__,
- `Instaloader Issues labeled 'question' <https://github.com/instaloader/instaloader/issues?q=is%3Aissue+is%3Aopen+label%3Aquestion>`__.
Reporting Bugs
--------------
If you encounter a bug, do not hesitate to report it in our
`Issue Tracker <https://github.com/instaloader/instaloader/issues>`__. When
reporting a problem, please keep the following in mind:
- Ensure you **use the latest version** of Instaloader. The currently-installed
version can be found out with ``instaloader --version``.
- Check whether there is a valid solution in our :ref:`troubleshooting` section.
- Briefly **check whether the bug has already been reported**. If you find an
issue reporting the same bug you encountered, comment there rather than
opening a new issue. However, if unsure, please create a new issue.
- State **how the bug can be reproduced**, i.e. how Instaloader was invoked
when the problem occurred (of course, you may anonymize profile names etc.).
- Include all **error messages and tracebacks** in the report.
- Even if it seems obvious, describe **which behavior you expected**
instead of what actually happened.
- If we have closed an issue apparently inadvertently or inappropriately, please
let us know.
Writing Code or Improving the Documentation
-------------------------------------------
Improvements of the Instaloader source or its documentation can be proposed as a
`Pull Request <https://github.com/instaloader/instaloader/pulls>`__.
- Please base your Pull Request on
- ``master``, which will be released with the next minor release, if it is
- a bug fix that does not require extensive testing,
- an improvement to the documentation,
- ``upcoming/v4.X``, if it is
- a new feature,
- a bug fix that does require thorough testing before being released to a
final version.
- Keep Pull Requests focused: address a single problem or feature in a
self-contained change. Avoid bundling multiple independent fixes, refactorings,
or architectural changes into one pull request, as this makes it hard to
review, validate, or selectively merge them.
- Be mindful of backward compatibility: changes should preserve existing intended
behavior whenever possible.
- All Pull Requests are analyzed by `Pylint <https://www.pylint.org/>`__ for
error and syntax checking of the source and
`Mypy <https://github.com/python/mypy>`__ for type checking. You can run them
locally with::
pylint instaloader
mypy -m instaloader
- Improvements to the documentation are very welcome. The documentation is
created with `Sphinx <https://www.sphinx-doc.org/en/2.0/>`__, version 2,
and can be build locally using::
make -C docs html
- Feel free to create an issue to make sure someone from the Instaloader team
agrees that the change might be an improvement, or if you want to discuss
your basic proposal, before working on a pull request.
Proposing Features
------------------
.. goal-start
Instaloader's goal is to mimic the browser's behavior to access the data that
is available through the Instagram web interface, pack this data into complete
and easily-(re)usable python objects, and provide a user interface for the most
common downloading and metadata collection tasks, without changing any of the
online data.
.. goal-end
Prior spending effort on implementing a new feature, it might be appropriate to
clarify how it could fit into the project's scope or discuss implementation
details. If you feel the need to do so, please create a "feature suggestion".
- Instaloader already has plenty of features. **Check the documentation**
beforehand to ensure your desired suggestion is not already implemented.
- Briefly **ensure that your idea has not already been suggested**. If you find
an issue suggesting the same or a similar feature, share your thoughts in a
comment there, instead of opening a new issue.
- **Motivate the feature**, i.e.
- Provide us a **use case of the feature**: How could the user
invoke the new function? Which problem would it solve? If new information is
obtained, how would it be further processed?
- Describe already-working **alternatives of the feature** and how they
compare to your proposed feature.
- Briefly describe how your suggested feature **conforms with Instaloader's
project goal**.
- Explain your **solution ideas**. Describe your ideas on how the feature could
be implemented and the underlying problem could be solved. Also **describe
alternatives** that you have considered.
Sponsoring
----------
.. include:: sponsors.rst
:start-after: donations-start
:end-before: donations-end
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

+106
View File
@@ -0,0 +1,106 @@
.. meta::
:description:
Free command line tool to download photos from Instagram.
Scrapes public and private profiles, hashtags, stories, feeds,
saved media, and their metadata, comments and captions.
Written in Python.
.. title:: Instaloader — Download Instagram Photos and Metadata
Instaloader
===========
.. highlight:: none
**Instaloader** is a tool to download pictures (or videos) along with
their captions and other metadata from Instagram.
.. include:: ../README.rst
:start-after: badges-start
:end-before: badges-end
With `Python <https://www.python.org/>`__ installed, do::
$ pip3 install instaloader
$ instaloader profile [profile ...]
See :ref:`install` for more options on how to install Instaloader.
**Instaloader**
- downloads **public and private profiles, hashtags, user stories,
feeds and saved media**,
- downloads **comments, geotags and captions** of each post,
- automatically **detects profile name changes** and renames the target
directory accordingly,
- allows **fine-grained customization** of filters and where to store
downloaded media,
- automatically **resumes previously-interrupted** download iterations,
- is free `open source <https://github.com/instaloader/instaloader>`__
software written in Python.
::
instaloader [--comments] [--geotags]
[--stories] [--highlights] [--tagged] [--reels] [--igtv]
[--login YOUR-USERNAME] [--fast-update]
profile | "#hashtag" | %location_id |
:stories | :feed | :saved
See :ref:`download-pictures-from-instagram` for a detailed introduction on how
to use Instaloader to download pictures from Instagram.
Instaloader Documentation
-------------------------
.. toctree::
:maxdepth: 2
installation
basic-usage
cli-options
as-module
codesnippets
troubleshooting
contributing
Useful Links
------------
- `Git Repository (on GitHub) <https://github.com/instaloader/instaloader>`__
- `PyPI Project Page <https://pypi.org/project/instaloader/>`__
- `Issue Tracker / Bug Tracker <https://github.com/instaloader/instaloader/issues>`__
- `Version History <https://github.com/instaloader/instaloader/releases>`__
Contributing
------------
As an open source project, Instaloader heavily depends on the contributions from
its community. See :ref:`contributing` for how you may help Instaloader to
become an even greater tool.
Supporters
----------
.. include:: sponsors.rst
:start-after: donations-start
:end-before: donations-end
Disclaimer
----------
.. include:: ../README.rst
:start-after: disclaimer-start
:end-before: disclaimer-end
..
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
+41
View File
@@ -0,0 +1,41 @@
.. _install:
Install Instaloader
===================
.. highlight:: none
To **install Instaloader**,
#. Ensure that you have `Python <https://www.python.org/>`__, at least
version 3.8, and `pip <https://pypi.python.org/pypi/pip>`__
installed.
#. Then, install Instaloader using::
pip3 install instaloader
To **upgrade Instaloader** to its current version, do::
pip3 install --upgrade instaloader
**Alternative methods** for installing Instaloader:
- If you do not want to use pip, even though it is highly recommended,
and prefer to **install Instaloader manually**,
`Download the Source <https://github.com/instaloader/instaloader/releases/latest>`__,
extract the Zip or Tarball and run ``setup.py`` from there.
- On **Arch Linux**, you may install Instaloader using the
`Instaloader AUR package <https://aur.archlinux.org/packages/instaloader/>`__.
- On **Windows 10**, you may download the standalone executable from the
`current release page <https://github.com/instaloader/instaloader/releases/latest>`__.
- On **Android**, you can use Instaloader with `Termux <https://play.google.com/store/apps/details?id=com.termux>`__
after typing ``pkg install python`` and ``pip3 install instaloader``.
- To test the most current **pre-release** version of Instaloader::
pip3 install --pre instaloader
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

+117
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

+61
View File
@@ -0,0 +1,61 @@
Exceptions
^^^^^^^^^^
.. module:: instaloader
:no-index:
.. highlight:: python
.. currentmodule:: instaloader.exceptions
.. autoexception:: InstaloaderException
:no-show-inheritance:
.. autoexception:: ConnectionException
.. currentmodule:: instaloader
.. autoexception:: QueryReturnedBadRequestException
.. autoexception:: QueryReturnedForbiddenException
.. autoexception:: ProfileNotExistsException
.. autoexception:: ProfileHasNoPicsException
.. autoexception:: PrivateProfileNotFollowedException
.. 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
.. versionchanged:: 4.3
QueryReturnedNotFoundException now inherits ConnectionException
to retry on 404 errors.
.. autoexception:: TooManyRequestsException
.. autoexception:: AbortDownloadException
+10
View File
@@ -0,0 +1,10 @@
``Instaloader`` (Main Class)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. module:: instaloader
:no-index:
.. highlight:: python
.. autoclass:: Instaloader
:no-show-inheritance:
+24
View File
@@ -0,0 +1,24 @@
``InstaloaderContext`` (Low-level functions)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. module:: instaloader
:no-index:
.. highlight:: python
.. contents::
:backlinks: none
``InstaloaderContext``
""""""""""""""""""""""
.. autoclass:: InstaloaderContext
:no-show-inheritance:
``RateController``
""""""""""""""""""
.. autoclass:: RateController
:no-show-inheritance:
.. versionadded:: 4.5
+38
View File
@@ -0,0 +1,38 @@
Resumable Iterations
^^^^^^^^^^^^^^^^^^^^
.. module:: instaloader
:no-index:
.. highlight:: python
.. contents::
:backlinks: none
For many download targets, Instaloader is able to resume a
previously-interrupted iteration. It provides an interruptible
Iterator :class:`NodeIterator` and a context manager
:func:`resumable_iteration`, which we both present here.
.. versionadded:: 4.5
``NodeIterator``
""""""""""""""""
.. autoclass:: NodeIterator
:no-show-inheritance:
.. autoclass:: FrozenNodeIterator
:no-show-inheritance:
A serializable representation of a :class:`NodeIterator` instance, saving
its iteration state.
It can be serialized and deserialized with :func:`save_structure_to_file`
and :func:`load_structure_from_file`, as well as with :mod:`json` and
:mod:`pickle` thanks to being a :class:`~typing.NamedTuple`.
``resumable_iteration``
"""""""""""""""""""""""
.. autofunction:: resumable_iteration
+100
View File
@@ -0,0 +1,100 @@
Instagram Structures
^^^^^^^^^^^^^^^^^^^^
.. module:: instaloader
:no-index:
.. highlight:: python
.. contents::
:backlinks: none
Posts
"""""
.. autoclass:: Post
:no-show-inheritance:
Additionally, the following trivial structures are defined:
.. autoclass:: PostSidecarNode
:no-show-inheritance:
.. autoclass:: PostComment
:no-show-inheritance:
.. autoclass:: PostCommentAnswer
:no-show-inheritance:
.. autoclass:: PostLocation
:no-show-inheritance:
User Stories
""""""""""""
.. autoclass:: Story
:no-show-inheritance:
.. autoclass:: StoryItem
:no-show-inheritance:
Highlights
""""""""""
.. autoclass:: Highlight
:no-show-inheritance:
:inherited-members:
Bases: :class:`Story`
.. versionadded:: 4.1
Profiles
""""""""
.. autoclass:: Profile
:no-show-inheritance:
Hashtags
""""""""
.. autoclass:: Hashtag
:no-show-inheritance:
.. versionadded:: 4.4
TopSearchResults
""""""""""""""""
.. autoclass:: TopSearchResults
:no-show-inheritance:
.. versionadded:: 4.3
TitlePic
""""""""
.. autoclass:: TitlePic
:no-show-inheritance:
.. versionadded:: 4.8
Loading and Saving
""""""""""""""""""
:class:`Post`, :class:`StoryItem`, :class:`Profile`, :class:`Hashtag` and
:class:`FrozenNodeIterator` can be saved and loaded to/from JSON files.
.. autofunction:: load_structure
.. autofunction:: load_structure_from_file
.. autofunction:: get_json_structure
.. autofunction:: save_structure_to_file
LatestStamps
""""""""""""
.. autoclass:: LatestStamps
:no-show-inheritance:
+23
View File
@@ -0,0 +1,23 @@
.. _sponsors:
Sponsoring the development of Instaloader
=========================================
.. donations-start
.. include:: ../README.rst
:start-after: current-sponsors-start
:end-before: current-sponsors-end
It is a pleasure for us to share our Instaloader to the world, and we are proud
to have attracted such an active and motivating community, with so many users
who share their suggestions and ideas with us. Buying a community-sponsored beer
or coffee from time to time is very likely to further raise our passion for the
development of Instaloader.
| For Donations, we provide GitHub Sponsors page, a PayPal.Me link and a Bitcoin address.
| GitHub Sponsors: `Sponsor @aandergr on GitHub Sponsors <https://github.com/sponsors/aandergr>`__
| PayPal: `PayPal.me/aandergr <https://www.paypal.me/aandergr>`__
| BTC: 1Nst4LoadeYzrKjJ1DX9CpbLXBYE9RKLwY
.. donations-end
+92
View File
@@ -0,0 +1,92 @@
.. _troubleshooting:
Troubleshooting
===============
.. highlight:: python
429 Too Many Requests
---------------------
Instaloader has a logic to keep track of its requests to Instagram and to obey
their rate limits. The rate controller assumes that
- at one time, Instaloader is the only application that consumes requests, i.e.
neither the Instagram browser interface, nor a mobile app, nor another
Instaloader instance is running in parallel, and
- no requests had been consumed when Instaloader starts.
The latter one implies that restarting or reinstantiating Instaloader often
within short time is prone to cause a 429.
Since the behavior of the rate controller might change between different
versions of Instaloader, make sure to use the current version of Instaloader,
especially when encountering many 429 errors.
If a request is denied with a 429,
Instaloader retries the request as soon as the temporary ban is assumed to be
expired. In case the retry continuously fails for some reason, which should not
happen under normal conditions, consider adjusting the
:option:`--max-connection-attempts` option.
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,
:ref:`logged-in accesses<login>` do not seem to be affected.
Instaloader allows to adjust the rate controlling behavior by overriding
:class:`instaloader.RateController`.
Too many queries in the last time
---------------------------------
**"Too many queries in the last time"** is not an error. It is a notice that the
rate limit has almost been reached, according to Instaloader's own rate
accounting mechanism.
Instaloader allows to adjust the rate controlling behavior by overriding
:class:`instaloader.RateController`.
Private but not followed
------------------------
You have to follow a private account to access most of its associated
information.
Login error
-----------
Instaloader's login *should* work fine, both with and without
Two-Factor-Authentication. It also supports handling the *checkpoint challenge*,
issued when Instagram suspects authentication activity on your account, by
pointing the user to an URL to be opened in a browser.
Nevertheless, in :issue:`92`, :issue:`615`, :issue:`1150` and :issue:`1217`,
users reported problems with
logging in. We recommend to always keep the session file which Instaloader
creates when using :option:`--login`. If a session file is present,
:option:`--login` does not make use of the failure-prone login procedure.
Also, session files usually do not expire.
If you do not have a session file present, you may use the following script
(:example:`615_import_firefox_session.py`) to workaround login problems by
importing the session cookies from Firefox and bypassing Instaloader's login and
so still use Instaloader's logged-in functionality.
.. literalinclude:: codesnippets/615_import_firefox_session.py
To use this script,
#. Download the script: :example:`615_import_firefox_session.py`,
#. Login to Instagram in Firefox,
#. Execute the snippet, e.g. with ``python 615_import_firefox_session.py``,
#. Then, ``instaloader -l USERNAME`` should work fine.
This script also supports specifying a cookie file path, which may be useful if
you use multiple Firefox profiles or if your operating system has the directory
structure differently set up. Also, you can specify an alternative session file
path.
+2 -729
View File
@@ -1,733 +1,6 @@
#!/usr/bin/env python3
"""Tool to download pictures (or videos) and captions from Instagram, from a given set
of profiles (even if private), from your feed or from all followees of a given profile."""
from instaloader.__main__ import main
import re, json, datetime, shutil, os, time, random, sys, pickle, getpass, tempfile
from argparse import ArgumentParser
from io import BytesIO
from numbers import Real
from typing import List, Optional, Any, Dict, Callable
import requests, requests.utils
# To get version from setup.py for instaloader --version
import pkg_resources
try:
# pylint:disable=no-member
__version__ = pkg_resources.get_distribution('instaloader').version
except pkg_resources.DistributionNotFound:
__version__ = 'Run ./setup.py --version'
try:
# pylint:disable=wrong-import-position
import win_unicode_console
except ImportError:
WINUNICODE = False
else:
win_unicode_console.enable()
WINUNICODE = True
class InstaloaderException(Exception):
"""Base exception for this script"""
pass
class NonfatalException(InstaloaderException):
"""Base exception for errors which should not cause instaloader to stop"""
pass
class ProfileNotExistsException(NonfatalException):
pass
class ProfileHasNoPicsException(NonfatalException):
pass
class PrivateProfileNotFollowedException(NonfatalException):
pass
class LoginRequiredException(NonfatalException):
pass
class BadCredentialsException(InstaloaderException):
pass
class ConnectionException(InstaloaderException):
pass
def _log(*msg, sep='', end='\n', flush=False, quiet=False):
if not quiet:
print(*msg, sep=sep, end=end, flush=flush)
def get_json(name: str, session: requests.Session, max_id: int = 0, sleep: bool = True) -> Optional[Dict[str, Any]]:
"""Return JSON of a profile"""
if max_id == 0:
resp = session.get('https://www.instagram.com/'+name)
else:
resp = session.get('https://www.instagram.com/'+name, params={'max_id': max_id})
if sleep:
time.sleep(4 * random.random() + 1)
match = re.search('window\\._sharedData = .*<', resp.text)
if match is None:
return None
else:
return json.loads(match.group(0)[21:-2])
def get_username_by_id(session: requests.Session, profile_id: int) -> str:
"""To get the current username of a profile, given its unique ID, this function can be used.
session is required to be a logged-in (i.e. non-anonymous) session."""
tempsession = copy_session(session)
tempsession.headers.update({'Content-Type' : 'application/json'})
resp = tempsession.post('https://www.instagram.com/query/', data='q=ig_user(' +
str(profile_id) +')+%7B%0A++username%0A%7D%0A')
if resp.status_code == 200:
data = json.loads(resp.text)
if 'username' in data:
return json.loads(resp.text)['username']
raise ProfileNotExistsException("No profile found, the user may have blocked " +
"you (id: " + str(profile_id) + ").")
else:
if test_login(session):
raise ConnectionException("Username could not be determined due to connection error" +
" (id: "+ str(profile_id) +").")
raise LoginRequiredException("Login required to determine username (id: " +
str(profile_id) + ").")
def get_id_by_username(profile: str) -> int:
"""Each Instagram profile has its own unique ID which stays unmodified even if a user changes
his/her username. To get said ID, given the profile's name, you may call this function."""
data = get_json(profile, get_anonymous_session())
if len(data["entry_data"]) == 0 or "ProfilePage" not in data["entry_data"]:
raise ProfileNotExistsException("Profile {0} does not exist.".format(profile))
return int(data['entry_data']['ProfilePage'][0]['user']['id'])
def _epoch_to_string(epoch: Real) -> str:
return datetime.datetime.fromtimestamp(epoch).strftime('%Y-%m-%d_%H-%M-%S')
def get_followees(profile: str, session: requests.Session) -> List[Dict[str, Any]]:
"""
Retrieve list of followees of given profile
:param profile: Name of profile to lookup followees
:param session: Session belonging to a user, i.e. not an anonymous session
:return: List of followees (list of dictionaries), as returned by instagram server
"""
tmpsession = copy_session(session)
data = get_json(profile, tmpsession)
profile_id = data['entry_data']['ProfilePage'][0]['user']['id']
query = ["q=ig_user(" + profile_id + ")+%7B%0A"
"++follows.",
str(data['entry_data']['ProfilePage'][0]['user']['follows']['count']) +
")+%7B%0A"
"++++count%2C%0A"
"++++page_info+%7B%0A"
"++++++end_cursor%2C%0A"
"++++++has_next_page%0A"
"++++%7D%2C%0A"
"++++nodes+%7B%0A"
"++++++id%2C%0A"
"++++++full_name%2C%0A"
"++++++username%2C%0A"
"++++++followed_by+%7B%0A"
"++++++++count%0A"
"++++++%7D%0A"
"++++%7D%0A"
"++%7D%0A"
"%7D%0A"
"&ref=relationships%3A%3Afollow_list"]
tmpsession.headers.update(default_http_header())
tmpsession.headers.update({'Referer' : 'https://www.instagram.com/'+profile+'/following/'})
tmpsession.headers.update({'Content-Type' : 'application/json'})
resp = tmpsession.post('https://www.instagram.com/query/', data=query[0]+"first("+query[1])
if resp.status_code == 200:
data = json.loads(resp.text)
followees = []
while True:
for followee in data['follows']['nodes']:
followee['follower_count'] = followee.pop('followed_by')['count']
followees = followees + [followee]
if data['follows']['page_info']['has_next_page']:
resp = tmpsession.post('https://www.instagram.com/query/', data=query[0]
+ "after("
+ data['follows']['page_info']['end_cursor']
+ "%2C+" + query[1] )
data = json.loads(resp.text)
else:
break
return followees
if test_login(tmpsession):
raise ConnectionException("ConnectionError("+str(resp.status_code)+"): "
"unable to gather followees.")
raise LoginRequiredException("Login required to gather followees.")
def download_pic(name: str, url: str, date_epoch: Real, outputlabel: Optional[str] = None, quiet: bool = False) -> bool:
"""Downloads and saves picture with given url under given directory with given timestamp.
Returns true, if file was actually downloaded, i.e. updated."""
if outputlabel is None:
outputlabel = _epoch_to_string(date_epoch)
urlmatch = re.search('\\.[a-z]*\\?', url)
file_extension = url[-3:] if urlmatch is None else urlmatch.group(0)[1:-1]
filename = name.lower() + '/' + _epoch_to_string(date_epoch) + '.' + file_extension
if os.path.isfile(filename):
_log(outputlabel + ' exists', end=' ', flush=True, quiet=quiet)
return False
resp = get_anonymous_session().get(url, stream=True)
if resp.status_code == 200:
_log(outputlabel, end=' ', flush=True, quiet=quiet)
os.makedirs(name.lower(), exist_ok=True)
with open(filename, 'wb') as file:
resp.raw.decode_content = True
shutil.copyfileobj(resp.raw, file)
os.utime(filename, (datetime.datetime.now().timestamp(), date_epoch))
return True
else:
raise ConnectionException("File \'" + url + "\' could not be downloaded.")
def save_caption(name: str, date_epoch: Real, caption: str, shorter_output: bool = False, quiet: bool = False) -> None:
"""Updates picture caption"""
filename = name.lower() + '/' + _epoch_to_string(date_epoch) + '.txt'
pcaption = caption.replace('\n', ' ').strip()
caption = caption.encode("UTF-8")
if shorter_output:
pcaption = "txt"
else:
pcaption = '[' + ((pcaption[:29]+u"\u2026") if len(pcaption)>31 else pcaption) + ']'
try:
with open(filename, 'rb') as file:
file_caption = file.read()
if file_caption.replace(b'\r\n', b'\n') == caption.replace(b'\r\n', b'\n'):
try:
_log(pcaption + ' unchanged', end=' ', flush=True, quiet=quiet)
except UnicodeEncodeError:
_log('txt unchanged', end=' ', flush=True, quiet=quiet)
return None
else:
def get_filename(index):
return filename if index==0 else (filename[:-4] + '_old_' +
(str(0) if index<10 else str()) + str(index) + filename[-4:])
i = 0
while os.path.isfile(get_filename(i)):
i = i + 1
for index in range(i, 0, -1):
os.rename(get_filename(index-1), get_filename(index))
try:
_log(pcaption + ' updated', end=' ', flush=True, quiet=quiet)
except UnicodeEncodeError:
_log('txt updated', end=' ', flush=True, quiet=quiet)
except FileNotFoundError:
pass
try:
_log(pcaption, end=' ', flush=True, quiet=quiet)
except UnicodeEncodeError:
_log('txt', end=' ', flush=True, quiet=quiet)
os.makedirs(name.lower(), exist_ok=True)
with open(filename, 'wb') as text_file:
shutil.copyfileobj(BytesIO(caption), text_file)
os.utime(filename, (datetime.datetime.now().timestamp(), date_epoch))
def save_location(name: str, location_json: Dict[str, str], date_epoch: Real) -> None:
filename = name.lower() + '/' + _epoch_to_string(date_epoch) + '_location.txt'
location_string = location_json["name"]+"\n" + \
"https://maps.google.com/maps?q={0},{1}&ll={0},{1}\n" \
.format(location_json["lat"], location_json["lng"])
os.makedirs(name.lower(), exist_ok=True)
with open(filename, 'wb') as text_file:
shutil.copyfileobj(BytesIO(location_string.encode()), text_file)
os.utime(filename, (datetime.datetime.now().timestamp(), date_epoch))
def download_profilepic(name: str, url: str, quiet: bool = False) -> None:
"""Downloads and saves profile pic with given url."""
date_object = datetime.datetime.strptime(requests.head(url).headers["Last-Modified"], \
'%a, %d %b %Y %H:%M:%S GMT')
filename = name.lower() + '/' + _epoch_to_string(date_object.timestamp()) + \
'_UTC_profile_pic.' + url[-3:]
if os.path.isfile(filename):
_log(filename + ' already exists', quiet=quiet)
return None
match = re.search('http.*://.*instagram.*[^/]*\\.(com|net)/[^/]+/.', url)
if match is None:
raise ConnectionException("URL \'" + url + "\' could not be processed.")
index = len(match.group(0))-1
offset = 8 if match.group(0)[-1:] == 's' else 0
url = url[:index] + 's2048x2048' + ('/' if offset == 0 else str()) + url[index+offset:]
resp = get_anonymous_session().get(url, stream=True)
if resp.status_code == 200:
_log(filename, quiet=quiet)
os.makedirs(name.lower(), exist_ok=True)
with open(filename, 'wb') as file:
resp.raw.decode_content = True
shutil.copyfileobj(resp.raw, file)
os.utime(filename, (datetime.datetime.now().timestamp(), date_object.timestamp()))
else:
raise ConnectionException("File \'" + url + "\' could not be downloaded.")
def get_default_session_filename(username: str) -> str:
"""Returns default session filename for given username."""
dirname = tempfile.gettempdir() + "/" + ".instaloader-" + getpass.getuser()
filename = dirname + "/" + "session-" + username
return filename
def save_session(session: requests.Session, username: str, filename: Optional[str] = None, quiet: bool = False) -> None:
"""Saves requests.Session object."""
if filename is None:
filename = get_default_session_filename(username)
dirname = os.path.dirname(filename)
if not os.path.exists(dirname):
os.makedirs(dirname)
os.chmod(dirname, 0o700)
with open(filename, 'wb') as sessionfile:
os.chmod(filename, 0o600)
pickle.dump(requests.utils.dict_from_cookiejar(session.cookies), sessionfile)
_log("Saved session to %s." % filename, quiet=quiet)
def load_session(username: str, filename: Optional[str] = None, quiet: bool = False) -> requests.Session:
"""Returns loaded requests.Session object, or None if not found."""
if filename is None:
filename = get_default_session_filename(username)
try:
with open(filename, 'rb') as sessionfile:
session = requests.Session()
session.cookies = requests.utils.cookiejar_from_dict(pickle.load(sessionfile))
session.headers.update(default_http_header())
session.headers.update({'X-CSRFToken':session.cookies.get_dict()['csrftoken']})
_log("Loaded session from %s." % filename, quiet=quiet)
return session
except FileNotFoundError:
pass
def copy_session(session: requests.Session) -> requests.Session:
"""Duplicates a requests.Session."""
new = requests.Session()
new.cookies = \
requests.utils.cookiejar_from_dict(requests.utils.dict_from_cookiejar(session.cookies))
new.headers = session.headers
return new
def test_login(session: requests.Session) -> Optional[str]:
"""Returns the Instagram username to which given requests.Session object belongs, or None."""
if session is None:
return
data = get_json(str(), session)
if data['config']['viewer'] is None:
return
time.sleep(4 * random.random() + 1)
return data['config']['viewer']['username']
def default_http_header(empty_session_only: bool = False) -> Dict[str, str]:
"""Returns default HTTP header we use for requests."""
user_agent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' \
'(KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36'
header = { 'Accept-Encoding' : 'gzip, deflate', \
'Accept-Language' : 'en-US,en;q=0.8', \
'Connection' : 'keep-alive', \
'Content-Length' : '0', \
'Host' : 'www.instagram.com', \
'Origin' : 'https://www.instagram.com', \
'Referer' : 'https://www.instagram.com/', \
'User-Agent' : user_agent, \
'X-Instagram-AJAX' : '1', \
'X-Requested-With' : 'XMLHttpRequest'}
if empty_session_only:
del header['Host']
del header['Origin']
del header['Referer']
del header['X-Instagram-AJAX']
del header['X-Requested-With']
return header
def get_anonymous_session() -> requests.Session:
"""Returns our default anonymous requests.Session object."""
session = requests.Session()
session.cookies.update({'sessionid' : '', 'mid' : '', 'ig_pr' : '1', \
'ig_vw' : '1920', 'csrftoken' : '', \
's_network' : '', 'ds_user_id' : ''})
session.headers.update(default_http_header(empty_session_only=True))
return session
def get_session(user: str, passwd: str) -> requests.Session:
"""Log in to instagram with given username and password and return session object"""
session = requests.Session()
session.cookies.update({'sessionid' : '', 'mid' : '', 'ig_pr' : '1', \
'ig_vw' : '1920', 'csrftoken' : '', \
's_network' : '', 'ds_user_id' : ''})
session.headers.update(default_http_header())
resp = session.get('https://www.instagram.com/')
session.headers.update({'X-CSRFToken':resp.cookies['csrftoken']})
time.sleep(9 * random.random() + 3)
login = session.post('https://www.instagram.com/accounts/login/ajax/', \
data={'password':passwd,'username':user}, allow_redirects=True)
session.headers.update({'X-CSRFToken':login.cookies['csrftoken']})
time.sleep(5 * random.random())
if login.status_code == 200:
if user == test_login(session):
return session
else:
raise BadCredentialsException('Login error! Check your credentials!')
else:
raise ConnectionException('Login error! Connection error!')
def get_feed_json(session: requests.Session, end_cursor: str = None, sleep: bool = True) -> Dict[str, Any]:
"""
Get JSON of the user's feed.
:param session: Session belonging to a user, i.e. not an anonymous session
:param end_cursor: The end cursor, as from json["feed"]["media"]["page_info"]["end_cursor"]
:param sleep: Sleep between requests to instagram server
:return: JSON
"""
if end_cursor is None:
return get_json(str(), session, sleep=sleep)["entry_data"]["FeedPage"][0]
tmpsession = copy_session(session)
query = "q=ig_me()+%7B%0A++feed+%7B%0A++++media.after(" + end_cursor + "%2C+12)+%7B%0A"+\
"++++++nodes+%7B%0A++++++++id%2C%0A++++++++caption%2C%0A++++++++code%2C%0A++++++++"+\
"comments.last(4)+%7B%0A++++++++++count%2C%0A++++++++++nodes+%7B%0A++++++++++++"+\
"id%2C%0A++++++++++++created_at%2C%0A++++++++++++text%2C%0A++++++++++++"+\
"user+%7B%0A++++++++++++++id%2C%0A++++++++++++++profile_pic_url%2C%0A++++++++++++++"+\
"username%0A++++++++++++%7D%0A++++++++++%7D%2C%0A++++++++++"+\
"page_info%0A++++++++%7D%2C%0A++++++++comments_disabled%2C%0A++++++++"+\
"date%2C%0A++++++++dimensions+%7B%0A++++++++++height%2C%0A++++++++++"+\
"width%0A++++++++%7D%2C%0A++++++++display_src%2C%0A++++++++is_video%2C%0A++++++++"+\
"likes+%7B%0A++++++++++count%2C%0A++++++++++nodes+%7B%0A++++++++++++"+\
"user+%7B%0A++++++++++++++id%2C%0A++++++++++++++profile_pic_url%2C%0A++++++++++++++"+\
"username%0A++++++++++++%7D%0A++++++++++%7D%2C%0A++++++++++"+\
"viewer_has_liked%0A++++++++%7D%2C%0A++++++++location+%7B%0A++++++++++"+\
"id%2C%0A++++++++++has_public_page%2C%0A++++++++++name%0A++++++++%7D%2C%0A++++++++"+\
"owner+%7B%0A++++++++++id%2C%0A++++++++++blocked_by_viewer%2C%0A++++++++++"+\
"followed_by_viewer%2C%0A++++++++++full_name%2C%0A++++++++++"+\
"has_blocked_viewer%2C%0A++++++++++is_private%2C%0A++++++++++"+\
"profile_pic_url%2C%0A++++++++++requested_by_viewer%2C%0A++++++++++"+\
"username%0A++++++++%7D%2C%0A++++++++usertags+%7B%0A++++++++++"+\
"nodes+%7B%0A++++++++++++user+%7B%0A++++++++++++++"+\
"username%0A++++++++++++%7D%2C%0A++++++++++++x%2C%0A++++++++++++y%0A++++++++++"+\
"%7D%0A++++++++%7D%2C%0A++++++++video_url%2C%0A++++++++"+\
"video_views%0A++++++%7D%2C%0A++++++page_info%0A++++%7D%0A++%7D%2C%0A++id%2C%0A++"+\
"profile_pic_url%2C%0A++username%0A%7D%0A&ref=feed::show"
tmpsession.headers.update(default_http_header())
tmpsession.headers.update({'Referer' : 'https://www.instagram.com/'})
tmpsession.headers.update({'Content-Type' : 'application/json'})
resp = tmpsession.post('https://www.instagram.com/query/', data=query)
if sleep:
time.sleep(4 * random.random() + 1)
return json.loads(resp.text)
def get_location(node: Dict[str, str], session: requests.Session, sleep: bool = True) -> Dict[str, str]:
pic_json = get_json("p/" + node["code"], session, sleep=sleep)
if pic_json["entry_data"]["PostPage"][0]["media"]["location"] is not None:
location_json = get_json("explore/locations/" +
pic_json["entry_data"]["PostPage"][0]["media"]["location"]["id"],
session, sleep=sleep)
return location_json["entry_data"]["LocationsPage"][0]["location"]
def download_node(node: Dict[str, Any], session: requests.Session, name: str,
download_videos: bool = True, geotags: bool = False,
sleep: bool = True, shorter_output: bool = False, quiet: bool = False) -> bool:
"""
Download everything associated with one instagram node, i.e. picture, caption and video.
:param node: Node, as from media->nodes list in instagram's JSONs
:param session: Session
:param name: Name of profile to which this node belongs
:param download_videos: True, if videos should be downloaded
:param geotags: Download geotags
:param sleep: Sleep between requests to instagram server
:param shorter_output: Shorten log output by not printing captions
:param quiet: Suppress output
:return: True if something was downloaded, False otherwise, i.e. file was already there
"""
downloaded = download_pic(name, node["display_src"], node["date"], quiet=quiet)
if sleep:
time.sleep(1.75 * random.random() + 0.25)
if "caption" in node:
save_caption(name, node["date"], node["caption"], shorter_output, quiet)
else:
_log("<no caption>", end=' ', flush=True, quiet=quiet)
if node["is_video"] and download_videos:
video_data = get_json('p/' + node["code"], session, sleep=sleep)
download_pic(name,
video_data['entry_data']['PostPage'][0]['media']['video_url'],
node["date"], 'mp4', quiet=quiet)
if geotags:
location = get_location(node, session, sleep)
if location:
save_location(name, location, node["date"])
_log(quiet=quiet)
return downloaded
def download_feed_pics(session: requests.Session, max_count: int = None, fast_update: bool = False,
filter_func: Optional[Callable[[Dict[str, Dict[str, Any]]], bool]] = None,
download_videos: bool = True, geotags: bool = False,
shorter_output: bool = False, sleep: bool = True, quiet: bool = False) -> None:
"""
Download pictures from the user's feed.
Example to download up to the 20 pics the user last liked:
>>> download_feed_pics(load_session('USER'), max_count=20, fast_update=True,
>>> filter_func=lambda node: not node["likes"]["viewer_has_liked"])
:param session: Session belonging to a user, i.e. not an anonymous session
:param max_count: Maximum count of pictures to download
:param fast_update: If true, abort when first already-downloaded picture is encountered
:param filter_func: function(node), which returns True if given picture should not be downloaded
:param download_videos: True, if videos should be downloaded
:param geotags: Download geotags
:param shorter_output: Shorten log output by not printing captions
:param sleep: Sleep between requests to instagram server
:param quiet: Suppress output
"""
data = get_feed_json(session, sleep=sleep)
count = 1
while data["feed"]["media"]["page_info"]["has_next_page"]:
for node in data["feed"]["media"]["nodes"]:
if max_count is not None and count > max_count:
return
name = node["owner"]["username"]
if filter_func is not None and filter_func(node):
_log("<pic by %s skipped>" % name, flush=True, quiet=quiet)
continue
_log("[%3i] %s " % (count, name), end="", flush=True, quiet=quiet)
count += 1
downloaded = download_node(node, session, name,
download_videos=download_videos, geotags=geotags,
sleep=sleep, shorter_output=shorter_output, quiet=quiet)
if fast_update and not downloaded:
return
data = get_feed_json(session, end_cursor=data["feed"]["media"]["page_info"]["end_cursor"],
sleep=sleep)
def check_id(profile: str, session: requests.Session, json_data: Dict[str, Any], quiet: bool = False) -> str:
"""
Consult locally stored ID of profile with given name, check whether ID matches and whether name
has changed and return current name of the profile, and store ID of profile.
"""
profile_exists = len(json_data["entry_data"]) > 0 and "ProfilePage" in json_data["entry_data"]
is_logged_in = json_data["config"]["viewer"] is not None
try:
with open(profile + "/id", 'rb') as id_file:
profile_id = int(id_file.read())
if (not profile_exists) or \
(profile_id != int(json_data['entry_data']['ProfilePage'][0]['user']['id'])):
if is_logged_in:
newname = get_username_by_id(session, profile_id)
_log("Profile {0} has changed its name to {1}.".format(profile, newname),
quiet=quiet)
os.rename(profile, newname)
return newname
if profile_exists:
raise ProfileNotExistsException("Profile {0} does not match the stored "
"unique ID {1}.".format(profile, profile_id))
raise ProfileNotExistsException("Profile {0} does not exist. Please login to "
"update profile name. Unique ID: {1}."
.format(profile, profile_id))
return profile
except FileNotFoundError:
pass
if profile_exists:
os.makedirs(profile.lower(), exist_ok=True)
with open(profile + "/id", 'w') as text_file:
profile_id = json_data['entry_data']['ProfilePage'][0]['user']['id']
text_file.write(profile_id+"\n")
_log("Stored ID {0} for profile {1}.".format(profile_id, profile), quiet=quiet)
return profile
raise ProfileNotExistsException("Profile {0} does not exist.".format(profile))
def download(name: str, session: requests.Session, profile_pic_only: bool = False, download_videos: bool = True,
geotags: bool = False,
fast_update: bool = False, shorter_output: bool = False, sleep: bool = True, quiet: bool = False) -> None:
"""Download one profile"""
# pylint:disable=too-many-branches,too-many-locals
# Get profile main page json
data = get_json(name, session, sleep=sleep)
# check if profile does exist or name has changed since last download
# and update name and json data if necessary
name_updated = check_id(name, session, data, quiet=quiet)
if name_updated != name:
name = name_updated
data = get_json(name, session, sleep=sleep)
# Download profile picture
download_profilepic(name, data["entry_data"]["ProfilePage"][0]["user"]["profile_pic_url"],
quiet=quiet)
if sleep:
time.sleep(1.75 * random.random() + 0.25)
if profile_pic_only:
return
# Catch some errors
if data["entry_data"]["ProfilePage"][0]["user"]["is_private"]:
if data["config"]["viewer"] is None:
raise LoginRequiredException("profile %s requires login" % name)
if not data["entry_data"]["ProfilePage"][0]["user"]["followed_by_viewer"]:
raise PrivateProfileNotFollowedException("Profile %s: private but not followed." % name)
else:
if data["config"]["viewer"] is not None:
_log("profile %s could also be downloaded anonymously." % name, quiet=quiet)
if ("nodes" not in data["entry_data"]["ProfilePage"][0]["user"]["media"] or
len(data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"]) == 0) \
and not profile_pic_only:
raise ProfileHasNoPicsException("Profile %s: no pics found." % name)
# Iterate over pictures and download them
def get_last_id(data):
if len(data["entry_data"]) == 0 or \
len(data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"]) == 0:
return None
else:
data = data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"]
return int(data[len(data) - 1]["id"])
totalcount = data["entry_data"]["ProfilePage"][0]["user"]["media"]["count"]
count = 1
while get_last_id(data) is not None:
for node in data["entry_data"]["ProfilePage"][0]["user"]["media"]["nodes"]:
_log("[%3i/%3i] " % (count, totalcount), end="", flush=True, quiet=quiet)
count += 1
downloaded = download_node(node, session, name,
download_videos=download_videos, geotags=geotags,
sleep=sleep, shorter_output=shorter_output, quiet=quiet)
if fast_update and not downloaded:
return
data = get_json(name, session, max_id=get_last_id(data), sleep=sleep)
def get_logged_in_session(username: str, password: Optional[str] = None, quiet: bool = False) -> requests.Session:
"""Logs in and returns session, asking user for password if needed"""
if password is not None:
return get_session(username, password)
if quiet:
raise LoginRequiredException("Quiet mode requires given password or valid "
"session file.")
while password is None:
password = getpass.getpass(prompt="Enter Instagram password for %s: " % username)
try:
return get_session(username, password)
except BadCredentialsException as err:
print(err, file=sys.stderr)
password = None
def download_profiles(profilelist: List[str], username: Optional[str] = None, password: Optional[str] = None,
sessionfile: Optional[str] = None,
profile_pic_only: bool = False, download_videos: bool = True, geotags: bool = False,
fast_update: bool = False,
sleep: bool = True, shorter_output: bool = False, quiet: bool = False) -> None:
"""Download set of profiles and handle sessions"""
# pylint:disable=too-many-branches,too-many-locals
# Login, if desired
if username is not None:
session = load_session(username, sessionfile, quiet=quiet)
if username != test_login(session):
session = get_logged_in_session(username, password, quiet)
_log("Logged in as %s." % username, quiet=quiet)
else:
session = get_anonymous_session()
# Try block for KeyboardInterrupt (save session on ^C)
failedtargets = []
targets = set()
try:
# Generate set of targets
for pentry in profilelist:
if pentry[0] == '@':
_log("Retrieving followees of %s..." % pentry[1:], quiet=quiet)
followees = get_followees(pentry[1:], session)
targets.update([followee['username'] for followee in followees])
elif pentry == ":feed-all" and username is not None:
_log("Retrieving pictures from your feed...", quiet=quiet)
download_feed_pics(session, fast_update=fast_update,
download_videos=download_videos, geotags=geotags,
shorter_output=shorter_output, sleep=sleep, quiet=quiet)
elif pentry == ":feed-liked" and username is not None:
_log("Retrieving pictures you liked from your feed...", quiet=quiet)
download_feed_pics(session, fast_update=fast_update,
filter_func=lambda node: not node["likes"]["viewer_has_liked"],
download_videos=download_videos, geotags=geotags,
shorter_output=shorter_output, sleep=sleep, quiet=quiet)
else:
targets.add(pentry)
if len(targets) == 0:
_log("No profiles to download given.", quiet=quiet)
elif len(targets) > 1:
_log("Downloading %i profiles..." % len(targets), quiet=quiet)
# Iterate through targets list and download them
for target in targets:
try:
download(target, session, profile_pic_only, download_videos,
geotags, fast_update, shorter_output, sleep, quiet)
except NonfatalException as err:
failedtargets.append(target)
print(err, file=sys.stderr)
except KeyboardInterrupt:
print("\nInterrupted by user.", file=sys.stderr)
if len(targets) > 1 and len(failedtargets) > 0:
print("Errors occured (see above) while downloading profiles: %s." %
", ".join(failedtargets), file=sys.stderr)
# Save session if it is useful
if username is not None:
save_session(session, username, sessionfile, quiet=quiet)
def main():
parser = ArgumentParser(description=__doc__,
epilog="Report issues at https://github.com/Thammus/instaloader/issues.")
parser.add_argument('profile', nargs='*',
help='Name of profile to download; @<profile> to download all followees of '
'<profile>; or the special targets :feed-all or :feed-liked to '
'download pictures from your feed if --login is given (using '
'--fast-update is recommended).')
parser.add_argument('--version', action='version',
version=__version__)
parser.add_argument('-l', '--login', metavar='YOUR-USERNAME',
help='Login name for your Instagram account. Not needed to download public '\
'profiles, but if you want to download private profiles or all followees of '\
'some profile, you have to specify a username used to login.')
parser.add_argument('-p', '--password', metavar='YOUR-PASSWORD',
help='Password for your Instagram account. If --login is given and there is '\
'not yet a valid session file, you\'ll be prompted for your password if '\
'--password is not given. Specifying this option without --login has no '\
'effect.')
parser.add_argument('-f', '--sessionfile',
help='File to store session key, defaults to '+ \
get_default_session_filename("<login_name>"))
parser.add_argument('-P', '--profile-pic-only', action='store_true',
help='Only download profile picture')
parser.add_argument('-V', '--skip-videos', action='store_true',
help='Do not download videos')
parser.add_argument('-G', '--geotags', action='store_true',
help='Store geotags when available')
parser.add_argument('-F', '--fast-update', action='store_true',
help='Abort at encounter of first already-downloaded picture')
parser.add_argument('-S', '--no-sleep', action='store_true',
help='Do not sleep between actual downloads of pictures')
parser.add_argument('-O', '--shorter-output', action='store_true',
help='Do not display captions while downloading')
parser.add_argument('-q', '--quiet', action='store_true',
help='Disable user interaction, i.e. do not print messages (except errors) and fail ' \
'if login credentials are needed but not given.')
args = parser.parse_args()
try:
download_profiles(args.profile, args.login, args.password, args.sessionfile,
args.profile_pic_only, not args.skip_videos, args.geotags, args.fast_update,
not args.no_sleep, args.shorter_output, args.quiet)
except InstaloaderException as err:
raise SystemExit("Fatal error: %s" % err)
if __name__ == "__main__":
if __name__ == '__main__':
main()
+38
View File
@@ -0,0 +1,38 @@
"""Download pictures (or videos) along with their captions and other metadata from Instagram."""
__version__ = '4.15.4'
try:
# pylint:disable=wrong-import-position
import win_unicode_console # type: ignore
except ImportError:
pass
else:
win_unicode_console.enable()
from .exceptions import *
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)
+616
View File
@@ -0,0 +1,616 @@
"""Download pictures (or videos) along with their captions and other metadata from Instagram."""
import ast
import datetime
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, 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
from .lateststamps import LatestStamps
try:
import browser_cookie3
bc3_library = True
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] [--reels] [--igtv]
{2:{1}} [--login YOUR-USERNAME] [--fast-update]
{2:{1}} profile | "#hashtag" | %%location_id | :stories | :feed | :saved
{0} --help""".format(argv0, len(argv0), '')
def http_status_code_list(code_list_str: str) -> List[int]:
codes = [int(s) for s in code_list_str.split(',')]
for code in codes:
if not 100 <= code <= 599:
raise ArgumentTypeError("Invalid HTTP status code: {}".format(code))
return codes
def filterstr_to_filterfunc(filter_str: str, item_type: type):
"""Takes an --post-filter=... or --storyitem-filter=... filter
specification and makes a filter_func Callable out of it."""
# The filter_str is parsed, then all names occurring in its AST are replaced by loads to post.<name>. A
# function Post->bool is returned which evaluates the filter with the post as 'post' in its namespace.
class TransformFilterAst(ast.NodeTransformer):
def visit_Name(self, node: ast.Name):
if not isinstance(node.ctx, ast.Load):
raise InvalidArgumentException("Invalid filter: Modifying variables ({}) not allowed.".format(node.id))
if node.id == "datetime":
return node
if not hasattr(item_type, node.id):
raise InvalidArgumentException("Invalid filter: {} not a {} attribute.".format(node.id,
item_type.__name__))
new_node = ast.Attribute(ast.copy_location(ast.Name('item', ast.Load()), node), node.id,
ast.copy_location(ast.Load(), node))
return ast.copy_location(new_node, node)
input_filename = '<command line filter parameter>'
compiled_filter = compile(TransformFilterAst().visit(ast.parse(filter_str, filename=input_filename, mode='eval')),
filename=input_filename, mode='eval')
def filterfunc(item) -> bool:
# pylint:disable=eval-used
return bool(eval(compiled_filter, {'item': item, 'datetime': datetime.datetime}))
return filterfunc
def get_cookies_from_instagram(domain: str, browser: str, cookie_file: Optional[str] = None):
supported_browsers = {
"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,
"opera_gx": browser_cookie3.opera_gx,
"safari": browser_cookie3.safari,
"vivaldi": browser_cookie3.vivaldi,
}
if browser not in supported_browsers:
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))
for cookie in browser_cookies:
if domain in cookie.domain:
cookies[cookie.name] = cookie.value
if cookies:
print(f"Cookies loaded successfully from {browser}")
else:
raise LoginException(f"No cookies found for Instagram in {browser}, "
f"Are you logged in successfully in {browser}?")
return cookies
def import_session(browser: str, instaloader: Instaloader, cookiefile: Optional[str]):
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 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,
sessionfile: Optional[str] = None,
download_profile_pic: bool = True, download_posts=True,
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) -> ExitCode:
"""Download set of profiles, hashtags etc. and handle logging in and session files if desired."""
# Parse and generate filter function
post_filter = None
if post_filter_str is not None:
post_filter = filterstr_to_filterfunc(post_filter_str, Post)
instaloader.context.log('Only download posts with property "{}".'.format(post_filter_str))
storyitem_filter = None
if storyitem_filter_str is not None:
storyitem_filter = filterstr_to_filterfunc(storyitem_filter_str, StoryItem)
instaloader.context.log('Only download storyitems with property "{}".'.format(storyitem_filter_str))
latest_stamps = None
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):
instaloader.context.error("Warning: Parameter \"{}\" for --login is not a valid username.".format(username))
try:
instaloader.load_session_from_file(username, sessionfile)
except FileNotFoundError as err:
if sessionfile is not None:
print(err, file=sys.stderr)
instaloader.context.log("Session file does not exist yet - Logging in.")
if not instaloader.context.is_logged_in or username != instaloader.test_login():
if password is not None:
try:
instaloader.login(username, password)
except TwoFactorAuthRequiredException:
# https://github.com/instaloader/instaloader/issues/1217
instaloader.context.error("Warning: There have been reports of 2FA currently not working. "
"Consider importing session cookies from your browser with "
"--load-cookies.")
while True:
try:
code = input("Enter 2FA verification code: ")
instaloader.two_factor_login(code)
break
except BadCredentialsException as err:
print(err, file=sys.stderr)
pass
else:
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: 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:
if (target.endswith('.json') or target.endswith('.json.xz')) and os.path.isfile(target):
with instaloader.context.error_catcher(target):
structure = load_structure_from_file(instaloader.context, target)
if isinstance(structure, Post):
if post_filter is not None and not post_filter(structure):
instaloader.context.log("<{} ({}) skipped>".format(structure, target), flush=True)
continue
instaloader.context.log("Downloading {} ({})".format(structure, target))
instaloader.download_post(structure, os.path.dirname(target))
elif isinstance(structure, StoryItem):
if storyitem_filter is not None and not storyitem_filter(structure):
instaloader.context.log("<{} ({}) skipped>".format(structure, target), flush=True)
continue
instaloader.context.log("Attempting to download {} ({})".format(structure, target))
instaloader.download_storyitem(structure, os.path.dirname(target))
elif isinstance(structure, Profile):
raise InvalidArgumentException("Profile JSON are ignored. Pass \"{}\" to download that profile"
.format(structure.username))
else:
raise InvalidArgumentException("{} JSON file not supported as target"
.format(structure.__class__.__name__))
continue
# strip '/' characters to be more shell-autocompletion-friendly
target = target.rstrip('/')
with instaloader.context.error_catcher(target):
if re.match(r"^@[A-Za-z0-9._]+$", target):
instaloader.context.log("Retrieving followees of %s..." % target[1:])
profile = Profile.from_username(instaloader.context, target[1:])
for followee in profile.get_followees():
instaloader.save_profile_id(followee)
profiles.add(followee)
elif re.match(r"^#\w+$", target):
instaloader.download_hashtag(hashtag=target[1:], max_count=max_count, fast_update=fast_update,
post_filter=post_filter,
profile_pic=download_profile_pic, posts=download_posts)
elif re.match(r"^-[A-Za-z0-9-_]+$", target):
instaloader.download_post(Post.from_shortcode(instaloader.context, target[1:]), target)
elif re.match(r"^%[0-9]+$", target):
instaloader.download_location(location=target[1:], max_count=max_count, fast_update=fast_update,
post_filter=post_filter)
elif target == ":feed":
instaloader.download_feed_posts(fast_update=fast_update, max_count=max_count,
post_filter=post_filter)
elif target == ":stories":
instaloader.download_stories(fast_update=fast_update, storyitem_filter=storyitem_filter)
elif target == ":saved":
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_profile_content and not profile.is_private
):
raise ProfileNotExistsException("{} blocked you; But we download her anonymously."
.format(target))
else:
instaloader.context.error("{} blocked you.".format(target))
else:
profiles.add(profile)
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_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:
with instaloader.context.error_catcher():
anonymous_retry_profiles.add(anonymous_loader.check_profile_id(target,
latest_stamps))
instaloader.context.error("Warning: {} will be downloaded anonymously (\"{}\")."
.format(target, err))
else:
raise
else:
target_type = {
'#': 'hashtag',
'%': 'location',
'-': 'shortcode',
}.get(target[0], 'username')
raise ProfileNotExistsException('Invalid {} {}'.format(target_type, target))
if len(profiles) > 1:
instaloader.context.log("Downloading {} profiles: {}".format(len(profiles),
' '.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: 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,
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)
# User might be confused if Instaloader does nothing
if not targetlist:
if instaloader.context.is_logged_in:
# Instaloader did at least save a session file
instaloader.context.log("No targets were specified, thus nothing has been downloaded.")
else:
# Instaloader did not do anything
instaloader.context.log("usage:" + usage_string())
exit_code = ExitCode.INIT_FAILURE
return exit_code
def main():
parser = ArgumentParser(description=__doc__, add_help=False, usage=usage_string(),
epilog="The complete documentation can be found at "
"https://instaloader.github.io/.",
fromfile_prefix_chars='+')
g_targets = parser.add_argument_group("What to Download",
"Specify a list of targets. For each of these, Instaloader creates a folder "
"and downloads all posts. The following targets are supported:")
g_targets.add_argument('profile', nargs='*',
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. "
"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.")
g_targets.add_argument('_feed', nargs='*', metavar=":feed",
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.")
g_targets.add_argument('_saved', nargs='*', metavar=":saved",
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]",
help="Re-Download the given object.")
g_targets.add_argument('_fromfile', nargs='*', metavar="+args.txt",
help="Read targets (and options) from given textfile.")
g_post = parser.add_argument_group("What to Download of each Post")
g_prof = parser.add_argument_group("What to Download of each Profile")
g_prof.add_argument('-P', '--profile-pic-only', action='store_true',
help=SUPPRESS)
g_prof.add_argument('--no-posts', action='store_true',
help="Do not download regular posts.")
g_prof.add_argument('--no-profile-pic', action='store_true',
help='Do not download profile picture.')
g_post.add_argument('--slide', action='store',
help='Set what image/interval of a sidecar you want to download.')
g_post.add_argument('--no-pictures', action='store_true',
help='Do not download post pictures. Cannot be used together with --fast-update. '
'Implies --no-video-thumbnails, does not imply --no-videos.')
g_post.add_argument('-V', '--no-videos', action='store_true',
help='Do not download videos.')
g_post.add_argument('--no-video-thumbnails', action='store_true',
help='Do not download thumbnails of videos.')
g_post.add_argument('-G', '--geotags', action='store_true',
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.')
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. 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',
help='Template to write in txt file for each Post.')
g_post.add_argument('--storyitem-metadata-txt', action='append',
help='Template to write in txt file for each StoryItem.')
g_post.add_argument('--no-metadata-json', action='store_true',
help='Do not create a JSON file containing the metadata of each post.')
g_post.add_argument('--metadata-json', action='store_true',
help=SUPPRESS)
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.')
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.')
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.')
g_cond = parser.add_argument_group("Which Posts to Download")
g_cond.add_argument('-F', '--fast-update', action='store_true',
help='For each target, stop when encountering the first already-downloaded picture. This '
'flag is recommended when you use Instaloader to update your personal Instagram archive.')
g_cond.add_argument('--latest-stamps', nargs='?', metavar='STAMPSFILE', const=get_default_stamps_filename(),
help='Store the timestamps of latest media scraped for each profile. This allows updating '
'your personal Instagram archive even if you delete the destination directories. '
'If STAMPSFILE is not provided, defaults to ' + get_default_stamps_filename())
g_cond.add_argument('--post-filter', '--only-if', metavar='filter',
help='Expression that, if given, must evaluate to True for each post to be downloaded. Must be '
'a syntactically valid python expression. Variables are evaluated to '
'instaloader.Post attributes. Example: --post-filter=viewer_has_liked.')
g_cond.add_argument('--storyitem-filter', metavar='filter',
help='Expression that, if given, must evaluate to True for each storyitem to be downloaded. '
'Must be a syntactically valid python expression. Variables are evaluated to '
'instaloader.StoryItem attributes.')
g_cond.add_argument('-c', '--count',
help='Do not attempt to download more than COUNT posts. '
'Applies to #hashtag, %%location_id, :feed, and :saved.')
g_login = parser.add_argument_group('Login (Download Private Profiles)',
'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. 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',
help='Browser name to load cookies from Instagram')
g_login.add_argument('-B', '--cookiefile', metavar='COOKIE-FILE',
help='Cookie file of a profile to load cookies')
g_login.add_argument('-f', '--sessionfile',
help='Path for loading and storing session key file. '
'Defaults to ' + get_default_session_filename("<login_name>"))
g_login.add_argument('-p', '--password', metavar='YOUR-PASSWORD',
help='Password for your Instagram account. Without this option, '
'you\'ll be prompted for your password interactively if '
'there is not yet a valid session file.')
g_how = parser.add_argument_group('How to Download')
g_how.add_argument('--dirname-pattern',
help='Name of directory where to store posts. {profile} is replaced by the profile name, '
'{target} is replaced by the target you specified, i.e. either :feed, #hashtag or the '
'profile name. Defaults to \'{target}\'.')
g_how.add_argument('--filename-pattern',
help='Prefix of filenames for posts and stories, relative to the directory given with '
'--dirname-pattern. {profile} is replaced by the profile name,'
'{target} is replaced by the target you specified, i.e. either :feed'
'#hashtag or the profile name. Defaults to \'{date_utc}_UTC\'')
g_how.add_argument('--title-pattern',
help='Prefix of filenames for profile pics, hashtag profile pics, and highlight covers. '
'Defaults to \'{date_utc}_UTC_{typename}\' if --dirname-pattern contains \'{target}\' '
'or \'{dirname}\', or if --dirname-pattern is not specified. Otherwise defaults to '
'\'{target}_{date_utc}_UTC_{typename}\'.')
g_how.add_argument('--resume-prefix', metavar='PREFIX',
help='Prefix for filenames that are used to save the information to resume an interrupted '
'download.')
g_how.add_argument('--sanitize-paths', action='store_true',
help='Sanitize paths so that the resulting file and directory names are valid on both '
'Windows and Unix.')
g_how.add_argument('--no-resume', action='store_true',
help='Do not resume a previously-aborted download iteration, and do not save such information '
'when interrupted.')
g_how.add_argument('--use-aged-resume-files', action='store_true', help=SUPPRESS)
g_how.add_argument('--user-agent',
help='User Agent to use for HTTP requests. Defaults to \'{}\'.'.format(default_user_agent()))
g_how.add_argument('-S', '--no-sleep', action='store_true', help=SUPPRESS)
g_how.add_argument('--max-connection-attempts', metavar='N', type=int, default=3,
help='Maximum number of connection attempts until a request is aborted. Defaults to 3. If a '
'connection fails, it can be manually skipped by hitting CTRL+C. Set this to 0 to retry '
'infinitely.')
g_how.add_argument('--commit-mode', action='store_true', help=SUPPRESS)
g_how.add_argument('--request-timeout', metavar='N', type=float, default=300.0,
help='Seconds to wait before timing out a connection request. Defaults to 300.')
g_how.add_argument('--abort-on', type=http_status_code_list, metavar="STATUS_CODES",
help='Comma-separated list of HTTP status codes that cause Instaloader to abort, bypassing all '
'retry logic.')
g_how.add_argument('--no-iphone', action='store_true',
help='Do not attempt to download iPhone version of images and videos.')
g_misc = parser.add_argument_group('Miscellaneous Options')
g_misc.add_argument('-q', '--quiet', action='store_true',
help='Disable user interaction, i.e. do not print messages (except errors) and fail '
'if login credentials are needed but not given. This makes Instaloader suitable as a '
'cron job.')
g_misc.add_argument('-h', '--help', action='help', help='Show this help message and exit.')
g_misc.add_argument('--version', action='version', help='Show version number and exit.',
version=__version__)
args = parser.parse_args()
try:
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()
if ':feed-all' in args.profile or ':feed-liked' in args.profile:
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
if args.no_captions:
if not (post_metadata_txt_pattern or storyitem_metadata_txt_pattern):
post_metadata_txt_pattern = ''
storyitem_metadata_txt_pattern = ''
else:
raise InvalidArgumentException("--no-captions and --post-metadata-txt or --storyitem-metadata-txt "
"given; That contradicts.")
if args.no_resume and args.resume_prefix:
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 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
download_posts = not (args.no_posts or args.stories_only or args.profile_pic_only)
download_stories = args.stories or args.stories_only
loader = Instaloader(sleep=not args.no_sleep, quiet=args.quiet, user_agent=args.user_agent,
dirname_pattern=args.dirname_pattern, filename_pattern=args.filename_pattern,
download_pictures=not args.no_pictures,
download_videos=not args.no_videos, download_video_thumbnails=not args.no_video_thumbnails,
download_geotags=args.geotags,
download_comments=args.comments, save_metadata=not args.no_metadata_json,
compress_json=not args.no_compress_json,
post_metadata_txt_pattern=post_metadata_txt_pattern,
storyitem_metadata_txt_pattern=storyitem_metadata_txt_pattern,
max_connection_attempts=args.max_connection_attempts,
request_timeout=args.request_timeout,
resume_prefix=resume_prefix,
check_resume_bbd=not args.use_aged_resume_files,
slide=args.slide,
fatal_status_codes=args.abort_on,
iphone_support=not args.no_iphone,
title_pattern=args.title_pattern,
sanitize_paths=args.sanitize_paths)
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:
print("Fatal error: %s" % err)
exit_code = ExitCode.UNEXPECTED_ERROR
sys.exit(exit_code)
if __name__ == "__main__":
main()
+84
View File
@@ -0,0 +1,84 @@
class InstaloaderException(Exception):
"""Base exception for this script.
:note: This exception should not be raised directly."""
pass
class QueryReturnedBadRequestException(InstaloaderException):
pass
class QueryReturnedForbiddenException(InstaloaderException):
pass
class ProfileNotExistsException(InstaloaderException):
pass
class ProfileHasNoPicsException(InstaloaderException):
"""
.. deprecated:: 4.2.2
Not raised anymore.
"""
pass
class PrivateProfileNotFollowedException(InstaloaderException):
pass
class LoginRequiredException(InstaloaderException):
pass
class LoginException(InstaloaderException):
pass
class TwoFactorAuthRequiredException(LoginException):
pass
class InvalidArgumentException(InstaloaderException):
pass
class BadResponseException(InstaloaderException):
pass
class BadCredentialsException(LoginException):
pass
class ConnectionException(InstaloaderException):
pass
class PostChangedException(InstaloaderException):
""".. versionadded:: 4.2.2"""
pass
class QueryReturnedNotFoundException(ConnectionException):
pass
class TooManyRequestsException(ConnectionException):
pass
class IPhoneSupportDisabledException(InstaloaderException):
pass
class AbortDownloadException(Exception):
"""
Exception that is not catched in the error catchers inside the download loop and so aborts the
download loop.
This exception is not a subclass of ``InstaloaderException``.
.. versionadded:: 4.7
"""
pass
File diff suppressed because it is too large Load Diff
+943
View File
@@ -0,0 +1,943 @@
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, suppress
from datetime import datetime, timedelta
from functools import partial
from typing import Any, Callable, Dict, Iterator, List, Optional, Union
import requests
import requests.utils
from .exceptions import *
def copy_session(session: requests.Session, request_timeout: Optional[float] = None) -> requests.Session:
"""Duplicates a requests.Session."""
new = requests.Session()
new.cookies = requests.utils.cookiejar_from_dict(requests.utils.dict_from_cookiejar(session.cookies))
new.headers = session.headers.copy() # type: ignore
# Override default timeout behavior.
# Need to silence mypy bug for this. See: https://github.com/python/mypy/issues/2427
new.request = partial(new.request, timeout=request_timeout) # type: ignore
return new
def _embedded_query_data(obj: Any) -> Iterator[Dict[str, Any]]:
"""Yield the data of each GraphQL query result in JSON that a page of www.instagram.com embeds."""
if isinstance(obj, dict):
result = obj['__bbox'].get('result') if isinstance(obj.get('__bbox'), dict) else None
if isinstance(result, dict) and isinstance(result.get('data'), dict):
yield result['data']
for value in obj.values():
yield from _embedded_query_data(value)
elif isinstance(obj, list):
for value in obj:
yield from _embedded_query_data(value)
def default_user_agent() -> str:
return ('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 '
'(KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36')
def default_iphone_headers() -> Dict[str, Any]:
return {'User-Agent': 'Instagram 361.0.0.35.82 (iPad13,8; iOS 18_0; en_US; en-US; ' \
'scale=2.00; 2048x2732; 674117118) AppleWebKit/420+',
'x-ads-opt-out': '1',
'x-bloks-is-panorama-enabled': 'true',
'x-bloks-version-id': '16b7bd25c6c06886d57c4d455265669345a2d96625385b8ee30026ac2dc5ed97',
'x-fb-client-ip': 'True',
'x-fb-connection-type': 'wifi',
'x-fb-http-engine': 'Liger',
'x-fb-server-cluster': 'True',
'x-fb': '1',
'x-ig-abr-connection-speed-kbps': '2',
'x-ig-app-id': '124024574287414',
'x-ig-app-locale': 'en-US',
'x-ig-app-startup-country': 'US',
'x-ig-bandwidth-speed-kbps': '0.000',
'x-ig-capabilities': '36r/F/8=',
'x-ig-connection-speed': '{}kbps'.format(random.randint(1000, 20000)),
'x-ig-connection-type': 'WiFi',
'x-ig-device-locale': 'en-US',
'x-ig-mapped-locale': 'en-US',
'x-ig-timezone-offset': str((datetime.now().astimezone().utcoffset() or timedelta(seconds=0)).seconds),
'x-ig-www-claim': '0',
'x-pigeon-session-id': str(uuid.uuid4()),
'x-tigon-is-retry': 'False',
'x-whatsapp': '0'}
class InstaloaderContext:
"""Class providing methods for (error) logging and low-level communication with Instagram.
It is not thought to be instantiated directly, rather :class:`Instaloader` instances maintain a context
object.
For logging, it provides :meth:`log`, :meth:`error`, :meth:`error_catcher`.
It provides low-level communication routines :meth:`get_json`, :meth:`graphql_query`, :meth:`graphql_node_list`,
:meth:`get_and_write_raw` and implements mechanisms for rate controlling and error handling.
Further, it provides methods for logging in and general session handles, which are used by that routines in
class :class:`Instaloader`.
"""
def __init__(self, sleep: bool = True, quiet: bool = False, user_agent: Optional[str] = None,
max_connection_attempts: int = 3, request_timeout: float = 300.0,
rate_controller: Optional[Callable[["InstaloaderContext"], "RateController"]] = None,
fatal_status_codes: Optional[List[int]] = None,
iphone_support: bool = True):
self.user_agent = user_agent if user_agent is not None else default_user_agent()
self.request_timeout = request_timeout
self._session = self.get_anonymous_session()
self.username = None
self.user_id = None
self.sleep = sleep
self.quiet = quiet
self.max_connection_attempts = max_connection_attempts
self._graphql_page_length = 50
self.two_factor_auth_pending = None
self.iphone_support = iphone_support
self.iphone_headers = default_iphone_headers()
# error log, filled with error() and printed at the end of Instaloader.main()
self.error_log: List[str] = []
self._rate_controller = rate_controller(self) if rate_controller is not None else RateController(self)
# Can be set to True for testing, disables suppression of InstaloaderContext._error_catcher
self.raise_all_errors = False
# HTTP status codes that should cause an AbortDownloadException
self.fatal_status_codes = fatal_status_codes or []
# Cache profile from id (mapping from id to Profile)
self.profile_id_cache: Dict[int, Any] = dict()
@contextmanager
def anonymous_copy(self):
session = self._session
username = self.username
user_id = self.user_id
iphone_headers = self.iphone_headers
self._session = self.get_anonymous_session()
self.username = None
self.user_id = None
self.iphone_headers = default_iphone_headers()
try:
yield self
finally:
self._session.close()
self.username = username
self._session = session
self.user_id = user_id
self.iphone_headers = iphone_headers
@property
def is_logged_in(self) -> bool:
"""True, if this Instaloader instance is logged in."""
return bool(self.username)
def log(self, *msg, sep='', end='\n', flush=False):
"""Log a message to stdout that can be suppressed with --quiet."""
if not self.quiet:
print(*msg, sep=sep, end=end, flush=flush)
def error(self, msg, repeat_at_end=True):
"""Log a non-fatal error message to stderr, which is repeated at program termination.
:param msg: Message to be printed.
:param repeat_at_end: Set to false if the message should be printed, but not repeated at program termination."""
print(msg, file=sys.stderr)
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.
.. 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:
print("\nErrors or warnings occurred:", file=sys.stderr)
for err in self.error_log:
print(err, file=sys.stderr)
self._session.close()
@contextmanager
def error_catcher(self, extra_info: Optional[str] = None):
"""
Context manager to catch, print and record InstaloaderExceptions.
:param extra_info: String to prefix error message with."""
try:
yield
except InstaloaderException as err:
if extra_info:
self.error('{}: {}'.format(extra_info, err))
else:
self.error('{}'.format(err))
if self.raise_all_errors:
raise
def _default_http_header(self, empty_session_only: bool = False) -> Dict[str, str]:
"""Returns default HTTP header we use for requests."""
header = {'Accept-Encoding': 'gzip, deflate',
'Accept-Language': 'en-US,en;q=0.8',
'Connection': 'keep-alive',
'Content-Length': '0',
'Host': 'www.instagram.com',
'Origin': 'https://www.instagram.com',
'Referer': 'https://www.instagram.com/',
'User-Agent': self.user_agent,
'X-Instagram-AJAX': '1',
'X-Requested-With': 'XMLHttpRequest',
'x-ig-app-id': '936619743392459'}
if empty_session_only:
del header['Host']
del header['Origin']
del header['X-Instagram-AJAX']
del header['X-Requested-With']
return header
def get_anonymous_session(self) -> requests.Session:
"""Returns our default anonymous requests.Session object."""
session = requests.Session()
session.cookies.update({'sessionid': '', 'mid': '', 'ig_pr': '1',
'ig_vw': '1920', 'csrftoken': '',
's_network': '', 'ds_user_id': ''})
session.headers.update(self._default_http_header(empty_session_only=True))
# 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
return session
def save_session(self):
"""Not meant to be used directly, use :meth:`Instaloader.save_session`."""
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):
"""Not meant to be used directly, use :meth:`Instaloader.load_session`."""
session = requests.Session()
session.cookies = requests.utils.cookiejar_from_dict(sessiondata)
session.headers.update(self._default_http_header())
session.headers.update({'X-CSRFToken': session.cookies.get_dict()['csrftoken']})
# 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
self._session = session
self.username = username
def save_session_to_file(self, sessionfile):
"""Not meant to be used directly, use :meth:`Instaloader.save_session_to_file`."""
pickle.dump(self.save_session(), sessionfile)
def load_session_from_file(self, username, sessionfile):
"""Not meant to be used directly, use :meth:`Instaloader.load_session_from_file`."""
self.load_session(username, pickle.load(sessionfile))
def test_login(self) -> Optional[str]:
"""Not meant to be used directly, use :meth:`Instaloader.test_login`."""
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 BadCredentialsException: If the provided password is wrong.
:raises TwoFactorAuthRequiredException: First step of 2FA login done, now call
: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
http.client._MAXHEADERS = 200
session = requests.Session()
session.cookies.update({'sessionid': '', 'mid': '', 'ig_pr': '1',
'ig_vw': '1920', 'ig_cb': '1', 'csrftoken': '',
's_network': '', 'ds_user_id': ''})
session.headers.update(self._default_http_header())
# 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
# 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})
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/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 LoginException(
"Login error: JSON decode fail, {} - {}.".format(login.status_code, login.reason)
) from err
if resp_json.get('two_factor_required'):
two_factor_session = copy_session(session, self.request_timeout)
two_factor_session.headers.update({'X-CSRFToken': csrf_token})
two_factor_session.cookies.update({'csrftoken': csrf_token})
self.two_factor_auth_pending = (two_factor_session,
user,
resp_json['two_factor_info']['two_factor_identifier'])
raise TwoFactorAuthRequiredException("Login error: two-factor authentication required.")
if 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'],
resp_json['message']))
else:
raise LoginException("Login error: \"{}\" status.".format(resp_json['status']))
if 'authenticated' not in resp_json:
# Issue #472
if 'message' in resp_json:
raise LoginException("Login error: Unexpected response, \"{}\".".format(resp_json['message']))
else:
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 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 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
self.username = user
self.user_id = resp_json['userId']
def two_factor_login(self, two_factor_code):
"""Second step of login if 2FA is enabled.
Not meant to be used directly, use :meth:`Instaloader.two_factor_login`.
:raises InvalidArgumentException: No two-factor authentication pending.
:raises BadCredentialsException: 2FA verification code invalid.
.. versionadded:: 4.2"""
if not self.two_factor_auth_pending:
raise InvalidArgumentException("No two-factor authentication pending.")
(session, user, two_factor_id) = self.two_factor_auth_pending
login = session.post('https://www.instagram.com/accounts/login/ajax/two_factor/',
data={'username': user, 'verificationCode': two_factor_code, 'identifier': two_factor_id},
allow_redirects=True)
resp_json = login.json()
if resp_json['status'] != 'ok':
if 'message' in resp_json:
raise BadCredentialsException("2FA error: {}".format(resp_json['message']))
else:
raise BadCredentialsException("2FA error: \"{}\" status.".format(resp_json['status']))
session.headers.update({'X-CSRFToken': login.cookies['csrftoken']})
self._session = session
self.username = user
self.two_factor_auth_pending = None
def do_sleep(self):
"""Sleep a short time if self.sleep is set. Called before each request to instagram.com."""
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,
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: 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 AbortDownloadException: When the server responds with
'feedback_required'/'checkpoint_required'/'challenge_required'
:raises QueryReturnedBadRequestException: When the server responds with a 400 (and not
'feedback_required'/'checkpoint_required'/'challenge_required').
: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 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')
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 = ""
if resp.headers['Content-Type'].startswith('application/json'):
body = ': ' + resp.text[:500] + ('…' if len(resp.text) > 501 else '')
raise AbortDownloadException("Query to https://{}/{} responded with \"{} {}\"{}{}".format(
host, path, resp.status_code, resp.reason, redirect, body
))
while resp.is_redirect:
redirect_url = resp.headers['location']
self.log('\nHTTP redirect from https://{0}/{1} to {2}'.format(host, path, redirect_url))
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 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)):
resp = sess.get(redirect_url if redirect_url.endswith('/') else redirect_url + '/',
params=params, allow_redirects=False)
else:
break
if response_headers is not None:
response_headers.clear()
response_headers.update(resp.headers)
if resp.status_code == 400:
with suppress(json.decoder.JSONDecodeError):
if resp.json().get("message") in [
"feedback_required",
"checkpoint_required",
"challenge_required",
]:
# Raise AbortDownloadException in case of substantial Instagram
# requirements to stop producing more requests
raise AbortDownloadException(self._response_error(resp))
raise QueryReturnedBadRequestException(self._response_error(resp))
if resp.status_code == 404:
raise QueryReturnedNotFoundException(self._response_error(resp))
if resp.status_code == 429:
raise TooManyRequestsException(self._response_error(resp))
if resp.status_code != 200:
raise ConnectionException(self._response_error(resp))
else:
resp_json = resp.json()
if 'status' in resp_json and resp_json['status'] != "ok":
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)
if _attempt == self.max_connection_attempts:
if isinstance(err, QueryReturnedNotFoundException):
raise QueryReturnedNotFoundException(error_string) from err
else:
raise ConnectionException(error_string) from err
self.error(error_string + " [retrying; skip with ^C]", repeat_at_end=False)
try:
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:
self._rate_controller.handle_429('other')
return self.get_json(path=path, params=params, host=host, session=sess, _attempt=_attempt + 1,
response_headers=response_headers)
except KeyboardInterrupt:
self.error("[skipped by user]", repeat_at_end=False)
raise ConnectionException(error_string) from err
def graphql_query(self, query_hash: str, variables: 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.
: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.
"""
csrf = next((c.value for c in self._session.cookies
if c.name == 'csrftoken' and c.value), None)
if not csrf:
self._session.get('https://www.instagram.com/', timeout=self.request_timeout)
csrf = next((c.value for c in self._session.cookies
if c.name == 'csrftoken' and c.value), '')
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'] = '*/*'
tmpsession.headers['x-csrftoken'] = csrf
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={'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
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,
first_data: Optional[Dict[str, Any]] = None) -> Iterator[Dict[str, Any]]:
"""
Retrieve a list of GraphQL nodes.
.. deprecated:: 4.5
Use :class:`NodeIterator` instead, which provides more functionality.
"""
def _query():
query_variables['first'] = self._graphql_page_length
try:
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:
self._graphql_page_length = new_page_length
self.error("HTTP Error 400 (Bad Request) on GraphQL Query. Retrying with shorter page length.",
repeat_at_end=False)
return _query()
else:
raise
if first_data:
data = first_data
else:
data = _query()
yield from (edge['node'] for edge in data['edges'])
while data['page_info']['has_next_page']:
query_variables['after'] = data['page_info']['end_cursor']
data = _query()
yield from (edge['node'] for edge in data['edges'])
def get_iphone_json(self, path: str, params: Dict[str, Any]) -> Dict[str, Any]:
"""JSON request to ``i.instagram.com``.
:param path: URL, relative to ``i.instagram.com/``
:param params: GET parameters
: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.
.. versionadded:: 4.2.1"""
with copy_session(self._session, self.request_timeout) as tempsession:
# Set headers to simulate an API request from iPad
tempsession.headers['ig-intended-user-id'] = str(self.user_id)
tempsession.headers['x-pigeon-rawclienttime'] = '{:.6f}'.format(time.time())
# Add headers obtained from previous iPad request
tempsession.headers.update(self.iphone_headers)
# Extract key information from cookies if we haven't got it already from a previous request
header_cookies_mapping = {'x-mid': 'mid',
'ig-u-ds-user-id': 'ds_user_id',
'x-ig-device-id': 'ig_did',
'x-ig-family-device-id': 'ig_did',
'family_device_id': 'ig_did'}
# Map the cookie value to the matching HTTP request header
cookies = tempsession.cookies.get_dict().copy()
for key, value in header_cookies_mapping.items():
if value in cookies:
if key not in tempsession.headers:
tempsession.headers[key] = cookies[value]
else:
# Remove the cookie value if it's already specified as a header
tempsession.cookies.pop(value, None)
# Edge case for ig-u-rur header due to special string encoding in cookie
if 'rur' in cookies:
if 'ig-u-rur' not in tempsession.headers:
tempsession.headers['ig-u-rur'] = cookies['rur'].strip('\"').encode('utf-8') \
.decode('unicode_escape')
else:
tempsession.cookies.pop('rur', None)
# Remove headers specific to Desktop version
for header in ['Host', 'Origin', 'X-Instagram-AJAX', 'X-Requested-With', 'Referer']:
tempsession.headers.pop(header, None)
# No need for cookies if we have a bearer token
if 'authorization' in tempsession.headers:
tempsession.cookies.clear()
response_headers = dict() # type: Dict[str, Any]
response = self.get_json(path, params, 'i.instagram.com', tempsession, response_headers=response_headers)
# Extract the ig-set-* headers and use them in the next request
for key, value in response_headers.items():
if key.startswith('ig-set-'):
self.iphone_headers[key.replace('ig-set-', '')] = value
elif key.startswith('x-ig-set-'):
self.iphone_headers[key.replace('x-ig-set-', 'x-ig-')] = value
return response
def get_page_data(self, path: str) -> List[Dict[str, Any]]:
"""Load a page of ``www.instagram.com`` as a logged-out visitor and return the data it embeds.
Instagram renders public profile and post pages with the results of their GraphQL queries
embedded in the HTML, even when it refuses anonymous API requests for the same data.
:param path: URL, relative to ``www.instagram.com/``
:return: The ``data`` object of each query result that the page embeds, in page order.
:raises QueryReturnedNotFoundException: When the server responds with a 404.
:raises ConnectionException: When the request failed.
"""
self.do_sleep()
self._rate_controller.wait_before_query('other')
url = 'https://www.instagram.com/{0}'.format(path)
# Instagram only embeds the data when the request looks like a browser's page navigation.
headers = {'Accept': 'text/html', 'Sec-Fetch-Dest': 'document', 'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none'}
try:
with self.get_anonymous_session() as anonymous_session:
resp = anonymous_session.get(url, headers=headers, allow_redirects=False)
except requests.exceptions.RequestException as err:
raise ConnectionException("Page request to {}: {}".format(url, err)) from err
if resp.status_code == 404:
raise QueryReturnedNotFoundException(self._response_error(resp))
if resp.status_code != 200:
raise ConnectionException(self._response_error(resp))
data: List[Dict[str, Any]] = []
for script in re.finditer(r'<script type="application/json"[^>]*>(.*?)</script>', resp.text, re.DOTALL):
with suppress(json.decoder.JSONDecodeError):
data.extend(_embedded_query_data(json.loads(script.group(1))))
return data
def write_raw(self, resp: Union[bytes, requests.Response], filename: str) -> None:
"""Write raw response data into a file.
.. versionadded:: 4.2.1"""
self.log(filename, end=' ', flush=True)
with open(filename + '.temp', 'wb') as file:
if isinstance(resp, requests.Response):
shutil.copyfileobj(resp.raw, file)
else:
file.write(resp)
os.replace(filename + '.temp', filename)
def get_raw(self, url: str, _attempt=1) -> requests.Response:
"""Downloads a file anonymously.
:raises QueryReturnedNotFoundException: When the server responds with a 404.
:raises QueryReturnedForbiddenException: When the server responds with a 403.
:raises ConnectionException: When download failed.
.. versionadded:: 4.2.1"""
with self.get_anonymous_session() as anonymous_session:
resp = anonymous_session.get(url, stream=True)
if resp.status_code == 200:
resp.raw.decode_content = True
return resp
else:
if resp.status_code == 403:
# suspected invalid URL signature
raise QueryReturnedForbiddenException(self._response_error(resp))
if resp.status_code == 404:
# 404 not worth retrying.
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.
:raises QueryReturnedNotFoundException: When the server responds with a 404.
:raises QueryReturnedForbiddenException: When the server responds with a 403.
:raises ConnectionException: When download repeatedly failed."""
self.write_raw(self.get_raw(url), filename)
def head(self, url: str, allow_redirects: bool = False) -> requests.Response:
"""HEAD a URL anonymously.
:raises QueryReturnedNotFoundException: When the server responds with a 404.
:raises QueryReturnedForbiddenException: When the server responds with a 403.
:raises ConnectionException: When request failed.
.. versionadded:: 4.7.6
"""
with self.get_anonymous_session() as anonymous_session:
resp = anonymous_session.head(url, allow_redirects=allow_redirects)
if resp.status_code == 200:
return resp
else:
if resp.status_code == 403:
# suspected invalid URL signature
raise QueryReturnedForbiddenException(self._response_error(resp))
if resp.status_code == 404:
# 404 not worth retrying.
raise QueryReturnedNotFoundException(self._response_error(resp))
raise ConnectionException(self._response_error(resp))
class RateController:
"""
Class providing request tracking and rate controlling to stay within rate limits.
It can be overridden to change Instaloader's behavior regarding rate limits, for example to raise a custom
exception when the rate limit is hit::
import instaloader
class MyRateController(instaloader.RateController):
def sleep(self, secs):
raise MyCustomException()
L = instaloader.Instaloader(rate_controller=lambda ctx: MyRateController(ctx))
"""
def __init__(self, context: InstaloaderContext):
self._context = context
self._query_timestamps: Dict[str, List[float]] = dict()
self._earliest_next_request_time = 0.0
self._iphone_earliest_next_request_time = 0.0
def sleep(self, secs: float):
"""Wait given number of seconds."""
# Not static, to allow for the behavior of this method to depend on context-inherent properties, such as
# whether we are logged in.
time.sleep(secs)
def _dump_query_timestamps(self, current_time: float, failed_query_type: str):
windows = [10, 11, 20, 22, 30, 60]
self._context.error("Number of requests within last {} minutes grouped by type:"
.format('/'.join(str(w) for w in windows)),
repeat_at_end=False)
for query_type, times in self._query_timestamps.items():
reqs_in_sliding_window = [sum(t > current_time - w * 60 for t in times) for w in windows]
self._context.error(" {} {:>32}: {}".format(
"*" if query_type == failed_query_type else " ",
query_type,
" ".join("{:4}".format(reqs) for reqs in reqs_in_sliding_window)
), repeat_at_end=False)
def count_per_sliding_window(self, query_type: str) -> int:
"""Return how many requests of the given type can be done within a sliding window of 11 minutes.
This is called by :meth:`RateController.query_waittime` and allows to simply customize wait times before queries
at query_type granularity. Consider overriding :meth:`RateController.query_waittime` directly if you need more
control."""
# Not static, to allow for the count_per_sliding_window to depend on context-inherent properties, such as
# whether we are logged in.
return 75 if query_type == 'other' else 200
def _reqs_in_sliding_window(self, query_type: Optional[str], current_time: float, window: float) -> List[float]:
if query_type is not None:
# timestamps of type query_type
relevant_timestamps = self._query_timestamps[query_type]
else:
# all GraphQL queries, i.e. not 'iphone' or 'other'
graphql_query_timestamps = filter(lambda tp: tp[0] not in ['iphone', 'other'],
self._query_timestamps.items())
relevant_timestamps = [t for times in (tp[1] for tp in graphql_query_timestamps) for t in times]
return list(filter(lambda t: t > current_time - window, relevant_timestamps))
def query_waittime(self, query_type: str, current_time: float, untracked_queries: bool = False) -> float:
"""Calculate time needed to wait before query can be executed."""
per_type_sliding_window = 660
iphone_sliding_window = 1800
if query_type not in self._query_timestamps:
self._query_timestamps[query_type] = []
self._query_timestamps[query_type] = list(filter(lambda t: t > current_time - 60 * 60,
self._query_timestamps[query_type]))
def per_type_next_request_time():
reqs_in_sliding_window = self._reqs_in_sliding_window(query_type, current_time, per_type_sliding_window)
if len(reqs_in_sliding_window) < self.count_per_sliding_window(query_type):
return 0.0
else:
return min(reqs_in_sliding_window) + per_type_sliding_window + 6
def gql_accumulated_next_request_time():
if query_type in ['iphone', 'other']:
return 0.0
gql_accumulated_sliding_window = 600
gql_accumulated_max_count = 275
reqs_in_sliding_window = self._reqs_in_sliding_window(None, current_time, gql_accumulated_sliding_window)
if len(reqs_in_sliding_window) < gql_accumulated_max_count:
return 0.0
else:
return min(reqs_in_sliding_window) + gql_accumulated_sliding_window
def untracked_next_request_time():
if untracked_queries:
if query_type == "iphone":
reqs_in_sliding_window = self._reqs_in_sliding_window(query_type, current_time,
iphone_sliding_window)
self._iphone_earliest_next_request_time = min(reqs_in_sliding_window) + iphone_sliding_window + 18
else:
reqs_in_sliding_window = self._reqs_in_sliding_window(query_type, current_time,
per_type_sliding_window)
self._earliest_next_request_time = min(reqs_in_sliding_window) + per_type_sliding_window + 6
return max(self._iphone_earliest_next_request_time, self._earliest_next_request_time)
def iphone_next_request():
if query_type == "iphone":
reqs_in_sliding_window = self._reqs_in_sliding_window(query_type, current_time, iphone_sliding_window)
if len(reqs_in_sliding_window) >= 199:
return min(reqs_in_sliding_window) + iphone_sliding_window + 18
return 0.0
return max(0.0,
max(
per_type_next_request_time(),
gql_accumulated_next_request_time(),
untracked_next_request_time(),
iphone_next_request(),
) - current_time)
def wait_before_query(self, query_type: str) -> None:
"""This method is called before a query to Instagram.
It calls :meth:`RateController.query_waittime` to determine the time needed to wait and then calls
:meth:`RateController.sleep` to wait until the request can be made."""
waittime = self.query_waittime(query_type, time.monotonic(), False)
assert waittime >= 0
if waittime > 15:
formatted_waittime = ("{} seconds".format(round(waittime)) if waittime <= 666 else
"{} minutes".format(round(waittime / 60)))
self._context.log("\nToo many queries in the last time. Need to wait {}, until {:%H:%M}."
.format(formatted_waittime, datetime.now() + timedelta(seconds=waittime)))
if waittime > 0:
self.sleep(waittime)
if query_type not in self._query_timestamps:
self._query_timestamps[query_type] = [time.monotonic()]
else:
self._query_timestamps[query_type].append(time.monotonic())
def handle_429(self, query_type: str) -> None:
"""This method is called to handle a 429 Too Many Requests response.
It calls :meth:`RateController.query_waittime` to determine the time needed to wait and then calls
:meth:`RateController.sleep` to wait until we can repeat the same request."""
current_time = time.monotonic()
waittime = self.query_waittime(query_type, current_time, True)
assert waittime >= 0
self._dump_query_timestamps(current_time, query_type)
text_for_429 = ("Instagram responded with HTTP error \"429 - Too Many Requests\". Please do not run multiple "
"instances of Instaloader in parallel or within short sequence. Also, do not use any Instagram "
"App while Instaloader is running.")
self._context.error(textwrap.fill(text_for_429), repeat_at_end=False)
if waittime > 1.5:
formatted_waittime = ("{} seconds".format(round(waittime)) if waittime <= 666 else
"{} minutes".format(round(waittime / 60)))
self._context.error("The request will be retried in {}, at {:%H:%M}."
.format(formatted_waittime, datetime.now() + timedelta(seconds=waittime)),
repeat_at_end=False)
if waittime > 0:
self.sleep(waittime)
+126
View File
@@ -0,0 +1,126 @@
import configparser
from datetime import datetime, timezone
from typing import Optional
from os.path import dirname
from os import makedirs
class LatestStamps:
"""LatestStamps class.
Convenience class for retrieving and storing data from the :option:`--latest-stamps` file.
:param latest_stamps_file: path to file.
.. versionadded:: 4.8"""
PROFILE_ID = 'profile-id'
PROFILE_PIC = 'profile-pic'
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'
def __init__(self, latest_stamps_file):
self.file = latest_stamps_file
self.data = configparser.ConfigParser()
self.data.read(latest_stamps_file)
def _save(self):
if dn := dirname(self.file):
makedirs(dn, exist_ok=True)
with open(self.file, 'w') as f:
self.data.write(f)
def _ensure_section(self, section: str):
if not self.data.has_section(section):
self.data.add_section(section)
def get_profile_id(self, profile_name: str) -> Optional[int]:
"""Returns stored ID of profile."""
try:
return self.data.getint(profile_name, self.PROFILE_ID)
except (configparser.Error, ValueError):
return None
def save_profile_id(self, profile_name: str, profile_id: int):
"""Stores ID of profile."""
self._ensure_section(profile_name)
self.data.set(profile_name, self.PROFILE_ID, str(profile_id))
self._save()
def rename_profile(self, old_profile: str, new_profile: str):
"""Renames a profile."""
self._ensure_section(new_profile)
for option in [self.PROFILE_ID, self.PROFILE_PIC, self.POST_TIMESTAMP,
self.TAGGED_TIMESTAMP, self.IGTV_TIMESTAMP, self.STORY_TIMESTAMP]:
if self.data.has_option(old_profile, option):
value = self.data.get(old_profile, option)
self.data.set(new_profile, option, value)
self.data.remove_section(old_profile)
self._save()
def _get_timestamp(self, section: str, key: str) -> datetime:
try:
return datetime.strptime(self.data.get(section, key), self.ISO_FORMAT)
except (configparser.Error, ValueError):
return datetime.fromtimestamp(0, timezone.utc)
def _set_timestamp(self, section: str, key: str, timestamp: datetime):
self._ensure_section(section)
self.data.set(section, key, timestamp.strftime(self.ISO_FORMAT))
self._save()
def get_last_post_timestamp(self, profile_name: str) -> datetime:
"""Returns timestamp of last download of a profile's posts."""
return self._get_timestamp(profile_name, self.POST_TIMESTAMP)
def set_last_post_timestamp(self, profile_name: str, timestamp: datetime):
"""Sets timestamp of last download of a profile's posts."""
self._set_timestamp(profile_name, self.POST_TIMESTAMP, timestamp)
def get_last_tagged_timestamp(self, profile_name: str) -> datetime:
"""Returns timestamp of last download of a profile's tagged posts."""
return self._get_timestamp(profile_name, self.TAGGED_TIMESTAMP)
def set_last_tagged_timestamp(self, profile_name: str, timestamp: datetime):
"""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)
def set_last_igtv_timestamp(self, profile_name: str, timestamp: datetime):
"""Sets timestamp of last download of a profile's igtv posts."""
self._set_timestamp(profile_name, self.IGTV_TIMESTAMP, timestamp)
def get_last_story_timestamp(self, profile_name: str) -> datetime:
"""Returns timestamp of last download of a profile's stories."""
return self._get_timestamp(profile_name, self.STORY_TIMESTAMP)
def set_last_story_timestamp(self, profile_name: str, timestamp: datetime):
"""Sets timestamp of last download of a profile's stories."""
self._set_timestamp(profile_name, self.STORY_TIMESTAMP, timestamp)
def get_profile_pic(self, profile_name: str) -> str:
"""Returns filename of profile's last downloaded profile pic."""
try:
return self.data.get(profile_name, self.PROFILE_PIC)
except configparser.Error:
return ""
def set_profile_pic(self, profile_name: str, profile_pic: str):
"""Sets filename of profile's last downloaded profile pic."""
self._ensure_section(profile_name)
self.data.set(profile_name, self.PROFILE_PIC, profile_pic)
self._save()
+329
View File
@@ -0,0 +1,329 @@
import base64
import hashlib
import json
import os
from contextlib import contextmanager
from datetime import datetime, timedelta
from lzma import LZMAError
from typing import Any, Callable, Dict, Iterable, Iterator, NamedTuple, Optional, Tuple, TypeVar
from .exceptions import InvalidArgumentException
from .instaloadercontext import InstaloaderContext
class FrozenNodeIterator(NamedTuple):
query_hash: Optional[str]
query_variables: Dict
query_referer: Optional[str]
context_username: Optional[str]
total_index: int
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."""
FrozenNodeIterator.context_username.__doc__ = """The username who created the iterator, or ``None``."""
FrozenNodeIterator.total_index.__doc__ = """Number of items that have already been returned."""
FrozenNodeIterator.best_before.__doc__ = """Date when parts of the stored nodes might have expired."""
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')
class NodeIterator(Iterator[T]):
"""
Iterate the nodes within edges in a GraphQL pagination. Instances of this class are returned by many (but not all)
of Instaloader's :class:`Post`-returning functions (such as :meth:`Profile.get_posts` etc.).
What makes this iterator special is its ability to freeze/store its current state, e.g. to interrupt an iteration,
and later thaw/resume from where it left off.
You can freeze a NodeIterator with :meth:`NodeIterator.freeze`::
post_iterator = profile.get_posts()
try:
for post in post_iterator:
do_something_with(post)
except KeyboardInterrupt:
save("resume_information.json", post_iterator.freeze())
and later reuse it with :meth:`NodeIterator.thaw` on an equally-constructed NodeIterator::
post_iterator = profile.get_posts()
post_iterator.thaw(load("resume_information.json"))
(an appropriate method to load and save the :class:`FrozenNodeIterator` is e.g.
:func:`load_structure_from_file` and :func:`save_structure_to_file`.)
A :class:`FrozenNodeIterator` can only be thawn with a matching NodeIterator, i.e. a NodeIterator instance that has
been constructed with the same parameters as the instance that is represented by the :class:`FrozenNodeIterator` in
question. This is to ensure that an iteration cannot be resumed in a wrong, unmatching loop. As a quick way to
distinguish iterators that are saved e.g. in files, there is the :attr:`NodeIterator.magic` string: Two
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 = 12
_shelf_life = timedelta(days=29)
def __init__(self,
context: InstaloaderContext,
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,
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 {}
self._query_referer = query_referer
self._page_index = 0
self._total_index = 0
if first_data is not None:
self._data = first_data
self._best_before = datetime.now() + NodeIterator._shelf_life
else:
self._data = self._query()
self._first_node: Optional[Dict] = None
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
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
def __iter__(self):
return self
def __next__(self) -> T:
if self._page_index < len(self._data['edges']):
node = self._data['edges'][self._page_index]['node']
page_index, total_index = self._page_index, self._total_index
try:
self._page_index += 1
self._total_index += 1
except KeyboardInterrupt:
self._page_index, self._total_index = page_index, total_index
raise
item = self._node_wrapper(node)
if self._is_first is not None:
if self._is_first(item, self.first_item):
self._first_node = node
else:
if self._first_node is None:
self._first_node = node
return item
if self._data.get('page_info', {}).get('has_next_page'):
query_response = self._query(self._data['page_info']['end_cursor'])
if self._data['edges'] != query_response['edges'] and len(query_response['edges']) > 0:
page_index, data = self._page_index, self._data
try:
self._page_index = 0
self._data = query_response
except KeyboardInterrupt:
self._page_index, self._data = page_index, data
raise
return self.__next__()
raise StopIteration()
@property
def count(self) -> Optional[int]:
"""The ``count`` as returned by Instagram. This is not always the total count this iterator will yield."""
return self._data.get('count') if self._data is not None else None
@property
def total_index(self) -> int:
"""Number of items that have already been returned."""
return self._total_index
@property
def magic(self) -> str:
"""Magic string for easily identifying a matching iterator file for resuming (hash of some parameters)."""
magic_hash = hashlib.blake2b(digest_size=6)
magic_hash.update(json.dumps(
[self._query_hash, self._query_variables, self._query_referer, self._context.username]
).encode())
return base64.urlsafe_b64encode(magic_hash.digest()).decode()
@property
def first_item(self) -> Optional[T]:
"""
If this iterator has produced any items, returns the first item produced.
It is possible to override what is considered the first item (for example, to consider the
newest item in case items are not in strict chronological order) by passing a callback
function as the `is_first` parameter when creating the class.
.. versionadded:: 4.8
.. versionchanged:: 4.9.2
What is considered the first item can be overridden.
"""
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
if self._data is not None:
remaining_data = {**self._data,
'edges': (self._data['edges'][(max(self._page_index - 1, 0)):])}
return FrozenNodeIterator(
query_hash=self._query_hash,
query_variables=self._query_variables,
query_referer=self._query_referer,
context_username=self._context.username,
total_index=max(self.total_index - 1, 0),
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:
"""
Use this iterator for resuming from earlier iteration.
:raises InvalidArgumentException:
If
- the iterator on which this method is called has already been used, or
- the given :class:`FrozenNodeIterator` does not match, i.e. belongs to a different iteration.
"""
if self._total_index or self._page_index:
raise InvalidArgumentException("thaw() called on already-used iterator.")
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 or
self._doc_id != frozen.doc_id):
raise InvalidArgumentException("Mismatching resume information.")
if not frozen.best_before:
raise InvalidArgumentException("\"best before\" date missing.")
if frozen.remaining_data is None:
raise InvalidArgumentException("\"remaining_data\" missing.")
self._total_index = frozen.total_index
self._best_before = datetime.fromtimestamp(frozen.best_before)
self._data = frozen.remaining_data
if frozen.first_node is not None:
self._first_node = frozen.first_node
@contextmanager
def resumable_iteration(context: InstaloaderContext,
iterator: Iterable,
load: Callable[[InstaloaderContext, str], Any],
save: Callable[[FrozenNodeIterator, str], None],
format_path: Callable[[str], str],
check_bbd: bool = True,
enabled: bool = True) -> Iterator[Tuple[bool, int]]:
"""
High-level context manager to handle a resumable iteration that can be interrupted
with a :class:`KeyboardInterrupt` or an :class:`AbortDownloadException`.
It can be used as follows to automatically load a previously-saved state into the iterator, save the iterator's
state when interrupted, and delete the resume file upon completion::
post_iterator = profile.get_posts()
with resumable_iteration(
context=L.context,
iterator=post_iterator,
load=lambda _, path: FrozenNodeIterator(**json.load(open(path))),
save=lambda fni, path: json.dump(fni._asdict(), open(path, 'w')),
format_path=lambda magic: "resume_info_{}.json".format(magic)
) as (is_resuming, start_index):
for post in post_iterator:
do_something_with(post)
It yields a tuple (is_resuming, start_index).
When the passed iterator is not a :class:`NodeIterator`, it behaves as if ``resumable_iteration`` was not used,
just executing the inner body.
:param context: The :class:`InstaloaderContext`.
:param iterator: The fresh :class:`NodeIterator`.
:param load: Loads a FrozenNodeIterator from given path. The object is ignored if it has a different type.
:param save: Saves the given FrozenNodeIterator to the given path.
:param format_path: Returns the path to the resume file for the given magic.
:param check_bbd: Whether to check the best before date and reject an expired FrozenNodeIterator.
:param enabled: Set to False to disable all functionality and simply execute the inner body.
.. versionchanged:: 4.7
Also interrupt on :class:`AbortDownloadException`.
"""
if not enabled or not isinstance(iterator, NodeIterator):
yield False, 0
return
is_resuming = False
start_index = 0
resume_file_path = format_path(iterator.magic)
resume_file_exists = os.path.isfile(resume_file_path)
if resume_file_exists:
try:
fni = load(context, resume_file_path)
if not isinstance(fni, FrozenNodeIterator):
raise InvalidArgumentException("Invalid type.")
if check_bbd and fni.best_before and datetime.fromtimestamp(fni.best_before) < datetime.now():
raise InvalidArgumentException("\"Best before\" date exceeded.")
iterator.thaw(fni)
is_resuming = True
start_index = iterator.total_index
context.log("Resuming from {}.".format(resume_file_path))
except (InvalidArgumentException, LZMAError, json.decoder.JSONDecodeError, EOFError) as exc:
context.error("Warning: Not resuming from {}: {}".format(resume_file_path, exc))
try:
yield is_resuming, start_index
except (Exception, KeyboardInterrupt):
if os.path.dirname(resume_file_path):
os.makedirs(os.path.dirname(resume_file_path), exist_ok=True)
save(iterator.freeze(), resume_file_path)
context.log("\nSaved resume information to {}.".format(resume_file_path))
raise
if resume_file_exists:
os.unlink(resume_file_path)
context.log("Iteration complete, deleted resume information file {}.".format(resume_file_path))
View File
+46
View File
@@ -0,0 +1,46 @@
from typing import Any, Callable, Dict, Iterator, Optional, TypeVar
from .instaloadercontext import InstaloaderContext
T = TypeVar('T')
class SectionIterator(Iterator[T]):
"""Iterator for the new 'sections'-style responses.
.. versionadded:: 4.9"""
def __init__(self,
context: InstaloaderContext,
sections_extractor: Callable[[Dict[str, Any]], Dict[str, Any]],
media_wrapper: Callable[[Dict], T],
query_path: str,
first_data: Optional[Dict[str, Any]] = None):
self._context = context
self._sections_extractor = sections_extractor
self._media_wrapper = media_wrapper
self._query_path = query_path
self._data = first_data or self._query()
self._page_index = 0
self._section_index = 0
def __iter__(self):
return self
def _query(self, max_id: Optional[str] = None) -> Dict[str, Any]:
pagination_variables = {"max_id": max_id} if max_id is not None else {}
return self._sections_extractor(
self._context.get_json(self._query_path, params={"__a": 1, "__d": "dis", **pagination_variables})
)
def __next__(self) -> T:
if self._page_index < len(self._data['sections']):
media = self._data['sections'][self._page_index]['layout_content']['medias'][self._section_index]['media']
self._section_index += 1
if self._section_index >= len(self._data['sections'][self._page_index]['layout_content']['medias']):
self._section_index = 0
self._page_index += 1
return self._media_wrapper(media)
if self._data['more_available']:
self._page_index, self._section_index, self._data = 0, 0, self._query(self._data["next_max_id"])
return self.__next__()
raise StopIteration()
File diff suppressed because it is too large Load Diff
+49 -16
View File
@@ -1,36 +1,69 @@
#!/usr/bin/env python3
import re
import sys
import os
from setuptools import setup
if sys.version_info < (3, 5):
sys.exit('Instaloader requires Python >= 3.5.')
SRC = os.path.abspath(os.path.dirname(__file__))
def get_version():
with open(os.path.join(SRC, 'instaloader/__init__.py')) as f:
for line in f:
m = re.match("__version__ = '(.*)'", line)
if m:
return m.group(1)
raise SystemExit("Could not find version string.")
if sys.version_info < (3, 10):
sys.exit('Instaloader requires Python >= 3.10.')
requirements = ['requests>=2.25']
optional_requirements = {
'browser_cookie3': ['browser_cookie3>=0.19.1'],
}
keywords = (['instagram', 'instagram-scraper', 'instagram-client', 'instagram-feed', 'downloader', 'videos', 'photos',
'pictures', 'instagram-user-photos', 'instagram-photos', 'instagram-metadata', 'instagram-downloader',
'instagram-stories'])
# NOTE that many of the values defined in this file are duplicated on other places, such as the
# documentation.
setup(
name='instaloader',
version='1.2.1',
py_modules=['instaloader'],
url='https://github.com/Thammus/instaloader',
version=get_version(),
packages=['instaloader'],
package_data={'instaloader': ['py.typed']},
url='https://instaloader.github.io/',
license='MIT',
author='Alexander Graf, André Koch-Kramer',
author_email='mail@agraf.me, koch-kramer@web.de',
description='Tool to download Instagram pictures (or videos) and captions from a given set of '
'profiles (even if private), from your feed or from all followees of a given '
'profile.',
long_description=open('README.rst').read(),
install_requires=['requests>=2.4'],
python_requires='>=3.5',
entry_points={'console_scripts': ['instaloader=instaloader:main']},
zip_safe=True,
keywords='instagram downloader',
description='Download pictures (or videos) along with their captions and other metadata '
'from Instagram.',
long_description=open(os.path.join(SRC, 'README.rst')).read(),
install_requires=requirements,
python_requires='>=3.10',
extras_require=optional_requirements,
entry_points={'console_scripts': ['instaloader=instaloader.__main__:main']},
zip_safe=False,
keywords=keywords,
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Intended Audience :: End Users/Desktop',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'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'
+4
View File
@@ -0,0 +1,4 @@
# Instaloader Tests
Integration tests for Instaloader features that require Instagram access are maintained here:
https://github.com/instaloader/instaloader-tests