igdl 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
igdl-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,252 @@
1
+ Metadata-Version: 2.4
2
+ Name: igdl
3
+ Version: 0.1.0
4
+ Summary: CLI tool to download images and videos from Instagram posts
5
+ Author: viviciu
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/viviciu/igdl
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: instaloader
11
+
12
+ # igdl
13
+
14
+ A minimal CLI tool that downloads all images and videos from an Instagram post to a local folder. Paste a link, get the files.
15
+
16
+ - Supports posts, reels, carousels, and IGTV
17
+ - Downloads all slides in a carousel in one command
18
+ - Optional login to bypass Instagram rate limits
19
+ - Configurable download directory and file naming
20
+
21
+ ---
22
+
23
+ ## Requirements
24
+
25
+ - Python 3.8+
26
+ - macOS or Linux (Windows untested)
27
+
28
+ ---
29
+
30
+ ## Install
31
+
32
+ The easiest way is with [pipx](https://pipx.pypa.io), which handles all the Python environment setup automatically.
33
+
34
+ ```bash
35
+ # Install pipx if you don't have it
36
+ brew install pipx
37
+
38
+ # Install igdl
39
+ pipx install git+https://github.com/viviciu/igdl
40
+ ```
41
+
42
+ **zsh users:** add this to `~/.zshrc` to prevent zsh from misreading `?` in URLs:
43
+
44
+ ```bash
45
+ alias igdl='noglob igdl'
46
+ ```
47
+
48
+ Then reload: `source ~/.zshrc`
49
+
50
+ ### Manual install (without pipx)
51
+
52
+ <details>
53
+ <summary>Expand</summary>
54
+
55
+ ```bash
56
+ git clone https://github.com/viviciu/igdl ~/igdl
57
+ cd ~/igdl
58
+ python3 -m venv .venv
59
+ .venv/bin/pip install -e .
60
+ mkdir -p ~/.local/bin
61
+ ln -sf ~/igdl/.venv/bin/igdl ~/.local/bin/igdl
62
+ ```
63
+
64
+ Make sure `~/.local/bin` is in your PATH (`export PATH="$PATH:$HOME/.local/bin"` in `~/.zshrc`), then add the `noglob` alias above.
65
+
66
+ </details>
67
+
68
+ ---
69
+
70
+ ## Setup
71
+
72
+ Set your download directory once:
73
+
74
+ ```bash
75
+ igdl config --dir ~/Downloads/Instagram
76
+ ```
77
+
78
+ This saves to `~/.igdl/config.json`. Run `igdl config` with no flags to see the current setting.
79
+
80
+ ---
81
+
82
+ ## Usage
83
+
84
+ ```bash
85
+ # Download a post (images, videos, or carousels)
86
+ igdl https://www.instagram.com/p/SHORTCODE/
87
+
88
+ # Reels and IGTV work too
89
+ igdl https://www.instagram.com/reel/SHORTCODE/
90
+ igdl https://www.instagram.com/tv/SHORTCODE/
91
+ ```
92
+
93
+ Instagram rate-limits anonymous requests. Log in once to avoid this:
94
+
95
+ ```bash
96
+ igdl login your_username
97
+ ```
98
+
99
+ Your session is saved to `~/.igdl/session_<username>` and reused automatically on future downloads.
100
+
101
+ ---
102
+
103
+ ## Customizing file naming
104
+
105
+ Files are named using [instaloader's filename pattern](https://instaloader.github.io/module/instaloader.html#instaloader.Instaloader). The default is `{date_utc}_UTC` (e.g. `2026-04-08_11-09-53_UTC_1.jpg`).
106
+
107
+ To change it, edit `igdl/downloader.py` in the `_loader()` function and add a `filename_pattern` argument to the `Instaloader()` constructor:
108
+
109
+ ```python
110
+ L = instaloader.Instaloader(
111
+ filename_pattern="{owner_username}_{shortcode}", # ← change this
112
+ ...
113
+ )
114
+ ```
115
+
116
+ Available variables:
117
+
118
+ | Variable | Example |
119
+ |---|---|
120
+ | `{date_utc}` | `2026-04-08_11-09-53` |
121
+ | `{shortcode}` | `DW3lV16CO5K` |
122
+ | `{owner_username}` | `folchstudio` |
123
+ | `{mediaid}` | `3612345678901234567` |
124
+ | `{typename}` | `GraphImage`, `GraphVideo` |
125
+
126
+ ---
127
+
128
+ ## Project structure
129
+
130
+ ```
131
+ igdl/
132
+ pyproject.toml # package metadata and dependencies
133
+ igdl/
134
+ cli.py # argument parsing and command routing
135
+ config.py # read/write ~/.igdl/config.json
136
+ downloader.py # instaloader wrapper — core download logic
137
+ __init__.py
138
+ ```
139
+
140
+ Runtime data (created automatically, not in the repo):
141
+
142
+ ```
143
+ ~/.igdl/
144
+ config.json # stores your configured download directory
145
+ session_<username> # saved Instagram session from igdl login
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Dependencies
151
+
152
+ - [instaloader](https://instaloader.github.io) — handles Instagram auth, carousels, and all media types
153
+
154
+ ---
155
+
156
+ # Why do we install this way? How does install work?
157
+
158
+ This section answers various questions I had along the way, to inform me about why certain dev decisions were made and in which scenarios I'd want to execute them again.
159
+
160
+ ## Is a venv created every time you use the tool?
161
+
162
+ No — it's created once when you install, and just sits at ~/igdl/.venv/
163
+ permanently. Every time you run igdl, it uses that same existing environment.
164
+ Nothing is recreated.
165
+
166
+ ## Do most distributed CLI tools use a venv?
167
+
168
+ No. The more common approaches for tools meant to be shared are:
169
+
170
+ - pipx — the actual standard for Python CLI tools. It automatically creates an
171
+ isolated environment per tool behind the scenes, so the user never thinks
172
+ about it. pipx install igdl and you're done.
173
+ - brew — macOS users expect brew install sometool. Homebrew handles isolation
174
+ itself.
175
+ - A single script — if the tool has no dependencies or uses only stdlib, just
176
+ ship one .py file.
177
+
178
+ Venvs are really a development tool, not a distribution mechanism. The way we
179
+ set it up is fine for personal use, but if you were seriously publishing this
180
+ for others, you'd set it up for pipx instead.
181
+
182
+
183
+ ## We can't use a single script to run our CLI because we are using external dependencies.
184
+ Instaloader is an external dependency so a
185
+ single script won't work on its own. You'd need the user to have it
186
+ pre-installed, which isn't a great experience.
187
+
188
+ Where dependencies are declared: ~/igdl/pyproject.toml — the dependencies =
189
+ ["instaloader"] line. That's the one source of truth. When pip installs the
190
+ package, it reads that file and pulls instaloader in automatically.
191
+
192
+ ---
193
+ Every file/folder in ~/igdl/:
194
+
195
+ ~/igdl/
196
+ ├── pyproject.toml # Package metadata: name, version, dependencies,
197
+ │ # and which function to run when you type `igdl`
198
+ ├── README.md # Documentation (what you're editing)
199
+ ├── .gitignore # Tells git which files/folders to never commit
200
+
201
+ ├── igdl/ # The actual Python source code
202
+ │ ├── __init__.py # Makes this folder a Python package (can be empty)
203
+ │ ├── cli.py # Parses what you type and routes to the right action
204
+ │ ├── config.py # Reads/writes ~/.igdl/config.json
205
+ │ └── downloader.py # All the Instagram download logic
206
+
207
+ ├── igdl.egg-info/ # ← Auto-generated by pip when you ran `pip install -e .`
208
+ │ # Bookkeeping metadata pip uses to track the install.
209
+ │ # You never touch it, git ignores it, safe to delete
210
+ │ # (it regenerates itself next time you install)
211
+
212
+ └── .venv/ # Your isolated Python environment — Python itself,
213
+ pip, and instaloader all live here. Never committed.
214
+
215
+ ## What is `egg-info`? pip's 'notes to self'
216
+
217
+ egg-info is just pip's scratch notes about your local install. The name comes
218
+ from an old Python packaging format called "eggs" that predates the current
219
+ standard — the name stuck even though eggs themselves are gone.
220
+
221
+ ## Whats `__pycache__`? Optimization.
222
+
223
+ __init__.py doesn't create __pycache__ — Python itself does. Any time Python
224
+ runs a .py file, it compiles it to bytecode (a faster, pre-parsed version) and
225
+ caches it in __pycache__/. This happens automatically for every .py file that
226
+ gets imported. It's just a performance optimization — Python skips re-parsing
227
+ files it's already seen.
228
+
229
+ ---
230
+ ## Do the Python files communicate with each other?
231
+
232
+ Yes, through import. Look at the top of `cli.py`:
233
+
234
+ `from .config import get_download_dir, set_download_dir`
235
+ `from .downloader import download, login`
236
+
237
+ The . means "from this same package." So when you run igdl, Python loads
238
+ `cli.py`, which pulls in specific functions from config.py and downloader.py.
239
+ They don't run in parallel or send messages — it's more like **cli.py is in
240
+ charge and borrows tools from the other files when it needs them.**
241
+
242
+ The flow when you run igdl <url>:
243
+
244
+ cli.py ← entry point, reads your command
245
+ → config.py ← "what's the download directory?"
246
+ → downloader.py ← "go download this URL"
247
+ → instaloader ← (external library, lives in .venv)
248
+
249
+ ## What's `__init__.py`?
250
+ __init__.py just tells Python "this folder is a package, treat the files
251
+ inside as importable modules." **Without it, the from .config import ... lines
252
+ in cli.py wouldn't work.** It's essentially a flag file.
igdl-0.1.0/README.md ADDED
@@ -0,0 +1,241 @@
1
+ # igdl
2
+
3
+ A minimal CLI tool that downloads all images and videos from an Instagram post to a local folder. Paste a link, get the files.
4
+
5
+ - Supports posts, reels, carousels, and IGTV
6
+ - Downloads all slides in a carousel in one command
7
+ - Optional login to bypass Instagram rate limits
8
+ - Configurable download directory and file naming
9
+
10
+ ---
11
+
12
+ ## Requirements
13
+
14
+ - Python 3.8+
15
+ - macOS or Linux (Windows untested)
16
+
17
+ ---
18
+
19
+ ## Install
20
+
21
+ The easiest way is with [pipx](https://pipx.pypa.io), which handles all the Python environment setup automatically.
22
+
23
+ ```bash
24
+ # Install pipx if you don't have it
25
+ brew install pipx
26
+
27
+ # Install igdl
28
+ pipx install git+https://github.com/viviciu/igdl
29
+ ```
30
+
31
+ **zsh users:** add this to `~/.zshrc` to prevent zsh from misreading `?` in URLs:
32
+
33
+ ```bash
34
+ alias igdl='noglob igdl'
35
+ ```
36
+
37
+ Then reload: `source ~/.zshrc`
38
+
39
+ ### Manual install (without pipx)
40
+
41
+ <details>
42
+ <summary>Expand</summary>
43
+
44
+ ```bash
45
+ git clone https://github.com/viviciu/igdl ~/igdl
46
+ cd ~/igdl
47
+ python3 -m venv .venv
48
+ .venv/bin/pip install -e .
49
+ mkdir -p ~/.local/bin
50
+ ln -sf ~/igdl/.venv/bin/igdl ~/.local/bin/igdl
51
+ ```
52
+
53
+ Make sure `~/.local/bin` is in your PATH (`export PATH="$PATH:$HOME/.local/bin"` in `~/.zshrc`), then add the `noglob` alias above.
54
+
55
+ </details>
56
+
57
+ ---
58
+
59
+ ## Setup
60
+
61
+ Set your download directory once:
62
+
63
+ ```bash
64
+ igdl config --dir ~/Downloads/Instagram
65
+ ```
66
+
67
+ This saves to `~/.igdl/config.json`. Run `igdl config` with no flags to see the current setting.
68
+
69
+ ---
70
+
71
+ ## Usage
72
+
73
+ ```bash
74
+ # Download a post (images, videos, or carousels)
75
+ igdl https://www.instagram.com/p/SHORTCODE/
76
+
77
+ # Reels and IGTV work too
78
+ igdl https://www.instagram.com/reel/SHORTCODE/
79
+ igdl https://www.instagram.com/tv/SHORTCODE/
80
+ ```
81
+
82
+ Instagram rate-limits anonymous requests. Log in once to avoid this:
83
+
84
+ ```bash
85
+ igdl login your_username
86
+ ```
87
+
88
+ Your session is saved to `~/.igdl/session_<username>` and reused automatically on future downloads.
89
+
90
+ ---
91
+
92
+ ## Customizing file naming
93
+
94
+ Files are named using [instaloader's filename pattern](https://instaloader.github.io/module/instaloader.html#instaloader.Instaloader). The default is `{date_utc}_UTC` (e.g. `2026-04-08_11-09-53_UTC_1.jpg`).
95
+
96
+ To change it, edit `igdl/downloader.py` in the `_loader()` function and add a `filename_pattern` argument to the `Instaloader()` constructor:
97
+
98
+ ```python
99
+ L = instaloader.Instaloader(
100
+ filename_pattern="{owner_username}_{shortcode}", # ← change this
101
+ ...
102
+ )
103
+ ```
104
+
105
+ Available variables:
106
+
107
+ | Variable | Example |
108
+ |---|---|
109
+ | `{date_utc}` | `2026-04-08_11-09-53` |
110
+ | `{shortcode}` | `DW3lV16CO5K` |
111
+ | `{owner_username}` | `folchstudio` |
112
+ | `{mediaid}` | `3612345678901234567` |
113
+ | `{typename}` | `GraphImage`, `GraphVideo` |
114
+
115
+ ---
116
+
117
+ ## Project structure
118
+
119
+ ```
120
+ igdl/
121
+ pyproject.toml # package metadata and dependencies
122
+ igdl/
123
+ cli.py # argument parsing and command routing
124
+ config.py # read/write ~/.igdl/config.json
125
+ downloader.py # instaloader wrapper — core download logic
126
+ __init__.py
127
+ ```
128
+
129
+ Runtime data (created automatically, not in the repo):
130
+
131
+ ```
132
+ ~/.igdl/
133
+ config.json # stores your configured download directory
134
+ session_<username> # saved Instagram session from igdl login
135
+ ```
136
+
137
+ ---
138
+
139
+ ## Dependencies
140
+
141
+ - [instaloader](https://instaloader.github.io) — handles Instagram auth, carousels, and all media types
142
+
143
+ ---
144
+
145
+ # Why do we install this way? How does install work?
146
+
147
+ This section answers various questions I had along the way, to inform me about why certain dev decisions were made and in which scenarios I'd want to execute them again.
148
+
149
+ ## Is a venv created every time you use the tool?
150
+
151
+ No — it's created once when you install, and just sits at ~/igdl/.venv/
152
+ permanently. Every time you run igdl, it uses that same existing environment.
153
+ Nothing is recreated.
154
+
155
+ ## Do most distributed CLI tools use a venv?
156
+
157
+ No. The more common approaches for tools meant to be shared are:
158
+
159
+ - pipx — the actual standard for Python CLI tools. It automatically creates an
160
+ isolated environment per tool behind the scenes, so the user never thinks
161
+ about it. pipx install igdl and you're done.
162
+ - brew — macOS users expect brew install sometool. Homebrew handles isolation
163
+ itself.
164
+ - A single script — if the tool has no dependencies or uses only stdlib, just
165
+ ship one .py file.
166
+
167
+ Venvs are really a development tool, not a distribution mechanism. The way we
168
+ set it up is fine for personal use, but if you were seriously publishing this
169
+ for others, you'd set it up for pipx instead.
170
+
171
+
172
+ ## We can't use a single script to run our CLI because we are using external dependencies.
173
+ Instaloader is an external dependency so a
174
+ single script won't work on its own. You'd need the user to have it
175
+ pre-installed, which isn't a great experience.
176
+
177
+ Where dependencies are declared: ~/igdl/pyproject.toml — the dependencies =
178
+ ["instaloader"] line. That's the one source of truth. When pip installs the
179
+ package, it reads that file and pulls instaloader in automatically.
180
+
181
+ ---
182
+ Every file/folder in ~/igdl/:
183
+
184
+ ~/igdl/
185
+ ├── pyproject.toml # Package metadata: name, version, dependencies,
186
+ │ # and which function to run when you type `igdl`
187
+ ├── README.md # Documentation (what you're editing)
188
+ ├── .gitignore # Tells git which files/folders to never commit
189
+
190
+ ├── igdl/ # The actual Python source code
191
+ │ ├── __init__.py # Makes this folder a Python package (can be empty)
192
+ │ ├── cli.py # Parses what you type and routes to the right action
193
+ │ ├── config.py # Reads/writes ~/.igdl/config.json
194
+ │ └── downloader.py # All the Instagram download logic
195
+
196
+ ├── igdl.egg-info/ # ← Auto-generated by pip when you ran `pip install -e .`
197
+ │ # Bookkeeping metadata pip uses to track the install.
198
+ │ # You never touch it, git ignores it, safe to delete
199
+ │ # (it regenerates itself next time you install)
200
+
201
+ └── .venv/ # Your isolated Python environment — Python itself,
202
+ pip, and instaloader all live here. Never committed.
203
+
204
+ ## What is `egg-info`? pip's 'notes to self'
205
+
206
+ egg-info is just pip's scratch notes about your local install. The name comes
207
+ from an old Python packaging format called "eggs" that predates the current
208
+ standard — the name stuck even though eggs themselves are gone.
209
+
210
+ ## Whats `__pycache__`? Optimization.
211
+
212
+ __init__.py doesn't create __pycache__ — Python itself does. Any time Python
213
+ runs a .py file, it compiles it to bytecode (a faster, pre-parsed version) and
214
+ caches it in __pycache__/. This happens automatically for every .py file that
215
+ gets imported. It's just a performance optimization — Python skips re-parsing
216
+ files it's already seen.
217
+
218
+ ---
219
+ ## Do the Python files communicate with each other?
220
+
221
+ Yes, through import. Look at the top of `cli.py`:
222
+
223
+ `from .config import get_download_dir, set_download_dir`
224
+ `from .downloader import download, login`
225
+
226
+ The . means "from this same package." So when you run igdl, Python loads
227
+ `cli.py`, which pulls in specific functions from config.py and downloader.py.
228
+ They don't run in parallel or send messages — it's more like **cli.py is in
229
+ charge and borrows tools from the other files when it needs them.**
230
+
231
+ The flow when you run igdl <url>:
232
+
233
+ cli.py ← entry point, reads your command
234
+ → config.py ← "what's the download directory?"
235
+ → downloader.py ← "go download this URL"
236
+ → instaloader ← (external library, lives in .venv)
237
+
238
+ ## What's `__init__.py`?
239
+ __init__.py just tells Python "this folder is a package, treat the files
240
+ inside as importable modules." **Without it, the from .config import ... lines
241
+ in cli.py wouldn't work.** It's essentially a flag file.
File without changes
igdl-0.1.0/igdl/cli.py ADDED
@@ -0,0 +1,55 @@
1
+ import argparse
2
+ import sys
3
+
4
+ from .config import get_download_dir, set_download_dir
5
+ from .downloader import download, login
6
+
7
+
8
+ def main() -> None:
9
+ # Short-circuit URL detection before argparse sees it,
10
+ # so subparsers don't conflict with bare URL arguments.
11
+ if len(sys.argv) > 1 and sys.argv[1].startswith(("http://", "https://")):
12
+ url = sys.argv[1]
13
+ download_dir = get_download_dir()
14
+ if not download_dir:
15
+ print(
16
+ "No download directory configured.\n"
17
+ "Run: igdl config --dir ~/path/to/folder",
18
+ file=sys.stderr,
19
+ )
20
+ sys.exit(1)
21
+ download(url, download_dir)
22
+ return
23
+
24
+ parser = argparse.ArgumentParser(
25
+ prog="igdl",
26
+ description="Download images/videos from an Instagram post.",
27
+ epilog="Usage: igdl <url>",
28
+ )
29
+ subparsers = parser.add_subparsers(dest="command")
30
+
31
+ config_parser = subparsers.add_parser("config", help="Set configuration options")
32
+ config_parser.add_argument("--dir", metavar="PATH", help="Set the download directory")
33
+
34
+ login_parser = subparsers.add_parser("login", help="Save an Instagram session to bypass rate limits")
35
+ login_parser.add_argument("username", help="Your Instagram username")
36
+
37
+ args = parser.parse_args()
38
+
39
+ if args.command == "config":
40
+ if args.dir:
41
+ resolved = set_download_dir(args.dir)
42
+ print(f"Download directory set to: {resolved}")
43
+ else:
44
+ d = get_download_dir()
45
+ if d:
46
+ print(f"Download directory: {d}")
47
+ else:
48
+ print("No download directory set. Use: igdl config --dir <path>")
49
+ return
50
+
51
+ if args.command == "login":
52
+ login(args.username)
53
+ return
54
+
55
+ parser.print_help()
@@ -0,0 +1,30 @@
1
+ import json
2
+ import os
3
+ from pathlib import Path
4
+
5
+ CONFIG_DIR = Path.home() / ".igdl"
6
+ CONFIG_FILE = CONFIG_DIR / "config.json"
7
+
8
+
9
+ def get_config() -> dict:
10
+ if not CONFIG_FILE.exists():
11
+ return {}
12
+ with open(CONFIG_FILE) as f:
13
+ return json.load(f)
14
+
15
+
16
+ def get_download_dir() -> Path | None:
17
+ cfg = get_config()
18
+ d = cfg.get("download_dir")
19
+ return Path(d) if d else None
20
+
21
+
22
+ def set_download_dir(path: str) -> Path:
23
+ resolved = Path(path).expanduser().resolve()
24
+ resolved.mkdir(parents=True, exist_ok=True)
25
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
26
+ cfg = get_config()
27
+ cfg["download_dir"] = str(resolved)
28
+ with open(CONFIG_FILE, "w") as f:
29
+ json.dump(cfg, f, indent=2)
30
+ return resolved
@@ -0,0 +1,86 @@
1
+ import re
2
+ import sys
3
+ from pathlib import Path
4
+
5
+ import instaloader
6
+ from instaloader import LoginRequiredException, Post
7
+
8
+ SESSION_DIR = Path.home() / ".igdl"
9
+ SHORTCODE_RE = re.compile(r"instagram\.com/(?:p|reel|tv)/([A-Za-z0-9_-]+)")
10
+
11
+
12
+ def extract_shortcode(url: str) -> str:
13
+ m = SHORTCODE_RE.search(url)
14
+ if not m:
15
+ print("Error: could not parse Instagram URL. Expected a /p/, /reel/, or /tv/ link.", file=sys.stderr)
16
+ sys.exit(1)
17
+ return m.group(1)
18
+
19
+
20
+ def _loader(username: str | None = None, download_dir: Path | None = None) -> instaloader.Instaloader:
21
+ L = instaloader.Instaloader(
22
+ download_video_thumbnails=False,
23
+ save_metadata=False,
24
+ download_geotags=False,
25
+ download_comments=False,
26
+ post_metadata_txt_pattern="",
27
+ dirname_pattern=str(download_dir) if download_dir else "{target}",
28
+ )
29
+ if username:
30
+ session_file = SESSION_DIR / f"session_{username}"
31
+ if session_file.exists():
32
+ L.load_session_from_file(username, str(session_file))
33
+ return L
34
+
35
+
36
+ def _saved_usernames() -> list[str]:
37
+ return [
38
+ f.name[len("session_"):]
39
+ for f in SESSION_DIR.iterdir()
40
+ if f.name.startswith("session_")
41
+ ]
42
+
43
+
44
+ def download(url: str, download_dir: Path) -> None:
45
+ shortcode = extract_shortcode(url)
46
+
47
+ # Try with a saved session first, fall back to anonymous
48
+ usernames = _saved_usernames()
49
+ L = _loader(usernames[0] if usernames else None, download_dir=download_dir)
50
+
51
+ try:
52
+ post = Post.from_shortcode(L.context, shortcode)
53
+ print(f"Downloading post by @{post.owner_username} to {download_dir}/")
54
+ L.download_post(post, target=shortcode)
55
+ print("Done.")
56
+ except LoginRequiredException:
57
+ print(
58
+ "Error: Instagram requires a login to download this post.\n"
59
+ "Run `igdl login` to save your session, then try again.",
60
+ file=sys.stderr,
61
+ )
62
+ sys.exit(1)
63
+ except instaloader.exceptions.InstaloaderException as e:
64
+ print(f"Error: {e}", file=sys.stderr)
65
+ sys.exit(1)
66
+
67
+
68
+ def login(username: str) -> None:
69
+ L = instaloader.Instaloader()
70
+ import getpass
71
+ password = getpass.getpass(f"Instagram password for {username}: ")
72
+ try:
73
+ L.login(username, password)
74
+ SESSION_DIR.mkdir(parents=True, exist_ok=True)
75
+ session_file = str(SESSION_DIR / f"session_{username}")
76
+ L.save_session_to_file(session_file)
77
+ print(f"Session saved. You can now download private/rate-limited posts.")
78
+ except instaloader.exceptions.BadCredentialsException:
79
+ print("Error: incorrect username or password.", file=sys.stderr)
80
+ sys.exit(1)
81
+ except instaloader.exceptions.TwoFactorAuthRequiredException:
82
+ code = input("Two-factor auth code: ")
83
+ L.two_factor_login(code)
84
+ session_file = str(SESSION_DIR / f"session_{username}")
85
+ L.save_session_to_file(session_file)
86
+ print("Session saved.")
@@ -0,0 +1,252 @@
1
+ Metadata-Version: 2.4
2
+ Name: igdl
3
+ Version: 0.1.0
4
+ Summary: CLI tool to download images and videos from Instagram posts
5
+ Author: viviciu
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/viviciu/igdl
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: instaloader
11
+
12
+ # igdl
13
+
14
+ A minimal CLI tool that downloads all images and videos from an Instagram post to a local folder. Paste a link, get the files.
15
+
16
+ - Supports posts, reels, carousels, and IGTV
17
+ - Downloads all slides in a carousel in one command
18
+ - Optional login to bypass Instagram rate limits
19
+ - Configurable download directory and file naming
20
+
21
+ ---
22
+
23
+ ## Requirements
24
+
25
+ - Python 3.8+
26
+ - macOS or Linux (Windows untested)
27
+
28
+ ---
29
+
30
+ ## Install
31
+
32
+ The easiest way is with [pipx](https://pipx.pypa.io), which handles all the Python environment setup automatically.
33
+
34
+ ```bash
35
+ # Install pipx if you don't have it
36
+ brew install pipx
37
+
38
+ # Install igdl
39
+ pipx install git+https://github.com/viviciu/igdl
40
+ ```
41
+
42
+ **zsh users:** add this to `~/.zshrc` to prevent zsh from misreading `?` in URLs:
43
+
44
+ ```bash
45
+ alias igdl='noglob igdl'
46
+ ```
47
+
48
+ Then reload: `source ~/.zshrc`
49
+
50
+ ### Manual install (without pipx)
51
+
52
+ <details>
53
+ <summary>Expand</summary>
54
+
55
+ ```bash
56
+ git clone https://github.com/viviciu/igdl ~/igdl
57
+ cd ~/igdl
58
+ python3 -m venv .venv
59
+ .venv/bin/pip install -e .
60
+ mkdir -p ~/.local/bin
61
+ ln -sf ~/igdl/.venv/bin/igdl ~/.local/bin/igdl
62
+ ```
63
+
64
+ Make sure `~/.local/bin` is in your PATH (`export PATH="$PATH:$HOME/.local/bin"` in `~/.zshrc`), then add the `noglob` alias above.
65
+
66
+ </details>
67
+
68
+ ---
69
+
70
+ ## Setup
71
+
72
+ Set your download directory once:
73
+
74
+ ```bash
75
+ igdl config --dir ~/Downloads/Instagram
76
+ ```
77
+
78
+ This saves to `~/.igdl/config.json`. Run `igdl config` with no flags to see the current setting.
79
+
80
+ ---
81
+
82
+ ## Usage
83
+
84
+ ```bash
85
+ # Download a post (images, videos, or carousels)
86
+ igdl https://www.instagram.com/p/SHORTCODE/
87
+
88
+ # Reels and IGTV work too
89
+ igdl https://www.instagram.com/reel/SHORTCODE/
90
+ igdl https://www.instagram.com/tv/SHORTCODE/
91
+ ```
92
+
93
+ Instagram rate-limits anonymous requests. Log in once to avoid this:
94
+
95
+ ```bash
96
+ igdl login your_username
97
+ ```
98
+
99
+ Your session is saved to `~/.igdl/session_<username>` and reused automatically on future downloads.
100
+
101
+ ---
102
+
103
+ ## Customizing file naming
104
+
105
+ Files are named using [instaloader's filename pattern](https://instaloader.github.io/module/instaloader.html#instaloader.Instaloader). The default is `{date_utc}_UTC` (e.g. `2026-04-08_11-09-53_UTC_1.jpg`).
106
+
107
+ To change it, edit `igdl/downloader.py` in the `_loader()` function and add a `filename_pattern` argument to the `Instaloader()` constructor:
108
+
109
+ ```python
110
+ L = instaloader.Instaloader(
111
+ filename_pattern="{owner_username}_{shortcode}", # ← change this
112
+ ...
113
+ )
114
+ ```
115
+
116
+ Available variables:
117
+
118
+ | Variable | Example |
119
+ |---|---|
120
+ | `{date_utc}` | `2026-04-08_11-09-53` |
121
+ | `{shortcode}` | `DW3lV16CO5K` |
122
+ | `{owner_username}` | `folchstudio` |
123
+ | `{mediaid}` | `3612345678901234567` |
124
+ | `{typename}` | `GraphImage`, `GraphVideo` |
125
+
126
+ ---
127
+
128
+ ## Project structure
129
+
130
+ ```
131
+ igdl/
132
+ pyproject.toml # package metadata and dependencies
133
+ igdl/
134
+ cli.py # argument parsing and command routing
135
+ config.py # read/write ~/.igdl/config.json
136
+ downloader.py # instaloader wrapper — core download logic
137
+ __init__.py
138
+ ```
139
+
140
+ Runtime data (created automatically, not in the repo):
141
+
142
+ ```
143
+ ~/.igdl/
144
+ config.json # stores your configured download directory
145
+ session_<username> # saved Instagram session from igdl login
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Dependencies
151
+
152
+ - [instaloader](https://instaloader.github.io) — handles Instagram auth, carousels, and all media types
153
+
154
+ ---
155
+
156
+ # Why do we install this way? How does install work?
157
+
158
+ This section answers various questions I had along the way, to inform me about why certain dev decisions were made and in which scenarios I'd want to execute them again.
159
+
160
+ ## Is a venv created every time you use the tool?
161
+
162
+ No — it's created once when you install, and just sits at ~/igdl/.venv/
163
+ permanently. Every time you run igdl, it uses that same existing environment.
164
+ Nothing is recreated.
165
+
166
+ ## Do most distributed CLI tools use a venv?
167
+
168
+ No. The more common approaches for tools meant to be shared are:
169
+
170
+ - pipx — the actual standard for Python CLI tools. It automatically creates an
171
+ isolated environment per tool behind the scenes, so the user never thinks
172
+ about it. pipx install igdl and you're done.
173
+ - brew — macOS users expect brew install sometool. Homebrew handles isolation
174
+ itself.
175
+ - A single script — if the tool has no dependencies or uses only stdlib, just
176
+ ship one .py file.
177
+
178
+ Venvs are really a development tool, not a distribution mechanism. The way we
179
+ set it up is fine for personal use, but if you were seriously publishing this
180
+ for others, you'd set it up for pipx instead.
181
+
182
+
183
+ ## We can't use a single script to run our CLI because we are using external dependencies.
184
+ Instaloader is an external dependency so a
185
+ single script won't work on its own. You'd need the user to have it
186
+ pre-installed, which isn't a great experience.
187
+
188
+ Where dependencies are declared: ~/igdl/pyproject.toml — the dependencies =
189
+ ["instaloader"] line. That's the one source of truth. When pip installs the
190
+ package, it reads that file and pulls instaloader in automatically.
191
+
192
+ ---
193
+ Every file/folder in ~/igdl/:
194
+
195
+ ~/igdl/
196
+ ├── pyproject.toml # Package metadata: name, version, dependencies,
197
+ │ # and which function to run when you type `igdl`
198
+ ├── README.md # Documentation (what you're editing)
199
+ ├── .gitignore # Tells git which files/folders to never commit
200
+
201
+ ├── igdl/ # The actual Python source code
202
+ │ ├── __init__.py # Makes this folder a Python package (can be empty)
203
+ │ ├── cli.py # Parses what you type and routes to the right action
204
+ │ ├── config.py # Reads/writes ~/.igdl/config.json
205
+ │ └── downloader.py # All the Instagram download logic
206
+
207
+ ├── igdl.egg-info/ # ← Auto-generated by pip when you ran `pip install -e .`
208
+ │ # Bookkeeping metadata pip uses to track the install.
209
+ │ # You never touch it, git ignores it, safe to delete
210
+ │ # (it regenerates itself next time you install)
211
+
212
+ └── .venv/ # Your isolated Python environment — Python itself,
213
+ pip, and instaloader all live here. Never committed.
214
+
215
+ ## What is `egg-info`? pip's 'notes to self'
216
+
217
+ egg-info is just pip's scratch notes about your local install. The name comes
218
+ from an old Python packaging format called "eggs" that predates the current
219
+ standard — the name stuck even though eggs themselves are gone.
220
+
221
+ ## Whats `__pycache__`? Optimization.
222
+
223
+ __init__.py doesn't create __pycache__ — Python itself does. Any time Python
224
+ runs a .py file, it compiles it to bytecode (a faster, pre-parsed version) and
225
+ caches it in __pycache__/. This happens automatically for every .py file that
226
+ gets imported. It's just a performance optimization — Python skips re-parsing
227
+ files it's already seen.
228
+
229
+ ---
230
+ ## Do the Python files communicate with each other?
231
+
232
+ Yes, through import. Look at the top of `cli.py`:
233
+
234
+ `from .config import get_download_dir, set_download_dir`
235
+ `from .downloader import download, login`
236
+
237
+ The . means "from this same package." So when you run igdl, Python loads
238
+ `cli.py`, which pulls in specific functions from config.py and downloader.py.
239
+ They don't run in parallel or send messages — it's more like **cli.py is in
240
+ charge and borrows tools from the other files when it needs them.**
241
+
242
+ The flow when you run igdl <url>:
243
+
244
+ cli.py ← entry point, reads your command
245
+ → config.py ← "what's the download directory?"
246
+ → downloader.py ← "go download this URL"
247
+ → instaloader ← (external library, lives in .venv)
248
+
249
+ ## What's `__init__.py`?
250
+ __init__.py just tells Python "this folder is a package, treat the files
251
+ inside as importable modules." **Without it, the from .config import ... lines
252
+ in cli.py wouldn't work.** It's essentially a flag file.
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ igdl/__init__.py
4
+ igdl/cli.py
5
+ igdl/config.py
6
+ igdl/downloader.py
7
+ igdl.egg-info/PKG-INFO
8
+ igdl.egg-info/SOURCES.txt
9
+ igdl.egg-info/dependency_links.txt
10
+ igdl.egg-info/entry_points.txt
11
+ igdl.egg-info/requires.txt
12
+ igdl.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ igdl = igdl.cli:main
@@ -0,0 +1 @@
1
+ instaloader
@@ -0,0 +1 @@
1
+ igdl
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "igdl"
7
+ version = "0.1.0"
8
+ description = "CLI tool to download images and videos from Instagram posts"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "viviciu" }]
13
+ dependencies = ["instaloader"]
14
+
15
+ [project.urls]
16
+ Homepage = "https://github.com/viviciu/igdl"
17
+
18
+ [project.scripts]
19
+ igdl = "igdl.cli:main"
igdl-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+