-
Notifications
You must be signed in to change notification settings - Fork 1
Developer Guide
How InstaDownload is put together - the architecture, the media downloading engine, and how to extend it. For setup and build commands, see Building.
┌───────────────────────────────────────────────┐
│ MainActivity.kt │
│ * Compose UI │
│ * URL validation │
│ * Shared-intent handling │
│ * saveToDownloads() > writes to storage │
│ * Haptics + permissions │
└─┬─────────────────────────────────────────────┘
│ getMediaItems(url) (on Dispatchers.IO)
▼
┌───────────────────────────────────────────────┐
│ InstagramDownloader.kt (singleton) │
│ * extractShortcode() │
│ * Strategy 1: tryEmbedPage() │
│ * Strategy 2: tryGraphQL() │
│ * downloadToStream() │
│ * OkHttp client + in memory cookie │
└───────────────────────────────────────────────┘
| Concern | Choice |
|---|---|
| Language | Kotlin 2.0.21 |
| UI | Jetpack Compose + Material 3 |
| Async | Kotlin Coroutines (Dispatchers.IO) |
| Networking | OkHttp 4.12 with a custom in memory CookieJar
|
| JSON |
org.json (GraphQL) + regex (embed page) |
| Min / Target SDK | 24 / 35 Min-Max |
Dependencys are in gradle/libs.versions.toml.
Instagram doesn't offer a public download API, so the app extracts media the way a browser would. getMediaItems() first pulls the shortcode from the URL, then tries two strategies in order. If the first throws, it falls back to the second; if both fail, it gives a combined error message.
url ──> extractShortcode() ──> [ Strategy 1: Embed ] ── if fail──> [ Strategy 2: GraphQL ] ──fail──> error combined, and then user reports the problem or fixes.
│ │
success success
▼ ▼
List<MediaResult> List<MediaResult>
Both strategies work off the post's shortcode, extracted with:
instagram\.com/(?:reel|p|tv)/([A-Za-z0-9_-]+)
Requests https://www.instagram.com/p/<shortcode>/embed/captioned/ with a mobile user agent. Instagram's embed page inlines the media in a double encoded JSON blob, so the parser:
- Looks for a video_url > returns a single video.
- Otherwise collects all unique display_url entries > single image or carousel.
- Falls back to the og:image meta tag if nothing else matched.
URLs are unescaped (\\\/ > /, & > &) before use. This path is fast and needs no cookies, which is why it is tried first.
If the embed page doesn't yield media, the app calls Instagram's private GraphQL endpoint:
- GETS - a GET to instagram.com so the server sets a csrftoken cookie (stored by the memory).
- POSTS to /graphql/query with variables={"shortcode":} and a doc_id, sending the X-CSRFToken and X-IG-App-ID headers.
- Parse data.xdt_shortcode_media:
- edge_sidecar_to_children > carousel, return every slide.
- is_video > single video
video_url. - otherwise > single image
display_url.
these are unofficial endpoints, Instagram changes them periodically. When both strategies fail, the error message includes both errors so issue report them.
saveToDownloads() in MainActivity.kt handles the storage differences across Android versions:
-
Android 10+ (API 29+): uses
MediaStore.DownloadswithIS_PENDING, streams the bytes in, then clears the pending flag. No storage permission needed. -
Android 9 and below: writes directly to the public
Downloadsdirectory, which requiresWRITE_EXTERNAL_STORAGE.
Filenames are timestamped (instagram_video_.mp4 / instagram_image_.jpg), with the index added so multiple files in one post don't collide.
The actual transfer is InstagramDownloader.downloadToStream(), which GETs the media URL and copies the response into the provided OutputStream.
Add a new extraction strategy. Write a tryX(shortcode): List<MediaResult> method in InstagramDownloader and slot it into the try/fallback chain in getMediaItems(). Keep the pattern of throwing a descriptive Exception on failure so the combined error stays useful.
Support a new link type. Update both the validation regex in MainActivity.isValidInstagramUrl() and SHORTCODE_REGEX in InstagramDownloader so validation and extraction stay in sync.
Fix broken extraction. When Instagram changes its format, the regexes/JSON paths in tryEmbedPage() / tryGraphQL() are what need updating. The doc_id in tryGraphQL() can also go stale and may need refreshing.