parth-dl 1.0.0__py3-none-any.whl

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.
parth_dl/__init__.py ADDED
@@ -0,0 +1,53 @@
1
+ """
2
+ parth-dl: Instagram Media Downloader
3
+ Download Instagram reels, posts, and profile pictures (public accounts only)
4
+
5
+ Author: Parth
6
+ License: MIT
7
+ """
8
+
9
+ __version__ = "1.0.0"
10
+ __author__ = "Parth"
11
+ __description__ = "Instagram media downloader for public content"
12
+
13
+ from .core import InstagramDownloader
14
+ from .utils import DownloadError, RateLimitError, NetworkError
15
+
16
+ __all__ = [
17
+ 'InstagramDownloader',
18
+ 'DownloadError',
19
+ 'RateLimitError',
20
+ 'NetworkError',
21
+ ]
22
+
23
+
24
+ def download(url, output_path=None, quality='best', verbose=False):
25
+ """
26
+ Quick download function
27
+
28
+ Args:
29
+ url: Instagram URL (reel, post, or profile)
30
+ output_path: Output file/directory path
31
+ quality: 'best' or 'worst'
32
+ verbose: Enable verbose logging
33
+
34
+ Returns:
35
+ Path to downloaded file(s)
36
+ """
37
+ downloader = InstagramDownloader(verbose=verbose)
38
+ return downloader.download(url, output_path, quality)
39
+
40
+
41
+ def get_info(url, verbose=False):
42
+ """
43
+ Get media information without downloading
44
+
45
+ Args:
46
+ url: Instagram URL
47
+ verbose: Enable verbose logging
48
+
49
+ Returns:
50
+ Dictionary with media information
51
+ """
52
+ downloader = InstagramDownloader(verbose=verbose)
53
+ return downloader.get_info(url)
parth_dl/cli.py ADDED
@@ -0,0 +1,169 @@
1
+ """
2
+ Command-line interface for parth-dl
3
+ """
4
+
5
+ import sys
6
+ import argparse
7
+ from . import __version__, __description__
8
+ from .core import InstagramDownloader
9
+ from .utils import DownloadError, RateLimitError, NetworkError, ValidationError
10
+
11
+
12
+ def print_banner():
13
+ """Print application banner"""
14
+ banner = f"""
15
+ ╔═══════════════════════════════════════════════════════════════╗
16
+ ║ parth-dl v{__version__} ║
17
+ ║ Instagram Media Downloader ║
18
+ ║ (Public Content Only) ║
19
+ ╚═══════════════════════════════════════════════════════════════╝
20
+ """
21
+ print(banner)
22
+
23
+
24
+ def create_parser():
25
+ """Create command-line argument parser"""
26
+ parser = argparse.ArgumentParser(
27
+ prog='parth-dl',
28
+ description=__description__,
29
+ formatter_class=argparse.RawDescriptionHelpFormatter,
30
+ epilog="""
31
+ Examples:
32
+ Download a reel:
33
+ parth-dl https://www.instagram.com/reel/ABC123/
34
+
35
+ Download a post:
36
+ parth-dl https://www.instagram.com/p/ABC123/
37
+
38
+ Download profile picture:
39
+ parth-dl https://www.instagram.com/username/
40
+
41
+ Download with custom output:
42
+ parth-dl https://www.instagram.com/reel/ABC123/ -o my_video.mp4
43
+
44
+ List available formats:
45
+ parth-dl https://www.instagram.com/reel/ABC123/ --list-formats
46
+
47
+ Enable verbose logging:
48
+ parth-dl https://www.instagram.com/reel/ABC123/ -v
49
+
50
+ Supported Content:
51
+ ✓ Reels (with audio)
52
+ ✓ Video posts (with audio)
53
+ ✓ Image posts (single & carousel)
54
+ ✓ Profile pictures
55
+ ✗ Stories (requires authentication)
56
+ ✗ Highlights (requires authentication)
57
+ ✗ Private accounts (not supported)
58
+
59
+ Note: This tool only works with PUBLIC Instagram content.
60
+ """
61
+ )
62
+
63
+ # Positional arguments
64
+ parser.add_argument(
65
+ 'url',
66
+ help='Instagram URL (post, reel, or profile)'
67
+ )
68
+
69
+ # Optional arguments
70
+ parser.add_argument(
71
+ '-o', '--output',
72
+ metavar='PATH',
73
+ help='Output file or directory path'
74
+ )
75
+
76
+ parser.add_argument(
77
+ '-q', '--quality',
78
+ choices=['best', 'worst'],
79
+ default='best',
80
+ help='Video quality preference (default: best)'
81
+ )
82
+
83
+ parser.add_argument(
84
+ '-v', '--verbose',
85
+ action='store_true',
86
+ help='Enable verbose/debug output'
87
+ )
88
+
89
+ parser.add_argument(
90
+ '--list-formats',
91
+ action='store_true',
92
+ help='List all available formats without downloading'
93
+ )
94
+
95
+ parser.add_argument(
96
+ '--no-rate-limit',
97
+ action='store_true',
98
+ help='Disable rate limiting (not recommended)'
99
+ )
100
+
101
+ parser.add_argument(
102
+ '--version',
103
+ action='version',
104
+ version=f'%(prog)s {__version__}'
105
+ )
106
+
107
+ return parser
108
+
109
+
110
+ def main():
111
+ """Main CLI entry point"""
112
+ parser = create_parser()
113
+ args = parser.parse_args()
114
+
115
+ # Print banner for non-quiet operations
116
+ if not args.list_formats:
117
+ print_banner()
118
+
119
+ try:
120
+ # Create downloader instance
121
+ downloader = InstagramDownloader(
122
+ verbose=args.verbose,
123
+ rate_limit=not args.no_rate_limit
124
+ )
125
+
126
+ # Execute command
127
+ if args.list_formats:
128
+ downloader.list_formats(args.url)
129
+ else:
130
+ downloader.download(
131
+ url=args.url,
132
+ output_path=args.output,
133
+ quality=args.quality
134
+ )
135
+
136
+ return 0
137
+
138
+ except KeyboardInterrupt:
139
+ print("\n\n[parth-dl] ⚠ Download cancelled by user", file=sys.stderr)
140
+ return 130
141
+
142
+ except ValidationError as e:
143
+ print(f"\n[parth-dl] ✗ Invalid input: {e}", file=sys.stderr)
144
+ return 1
145
+
146
+ except RateLimitError as e:
147
+ print(f"\n[parth-dl] ✗ Rate limit error: {e}", file=sys.stderr)
148
+ print("Tip: Wait a few minutes before trying again.", file=sys.stderr)
149
+ return 1
150
+
151
+ except NetworkError as e:
152
+ print(f"\n[parth-dl] ✗ Network error: {e}", file=sys.stderr)
153
+ print("Tip: Check your internet connection and try again.", file=sys.stderr)
154
+ return 1
155
+
156
+ except DownloadError as e:
157
+ print(f"\n[parth-dl] ✗ Download failed: {e}", file=sys.stderr)
158
+ return 1
159
+
160
+ except Exception as e:
161
+ print(f"\n[parth-dl] ✗ Unexpected error: {e}", file=sys.stderr)
162
+ if args.verbose:
163
+ import traceback
164
+ traceback.print_exc()
165
+ return 1
166
+
167
+
168
+ if __name__ == '__main__':
169
+ sys.exit(main())
parth_dl/core.py ADDED
@@ -0,0 +1,274 @@
1
+ """
2
+ Core downloader class - orchestrates extraction and downloading
3
+ """
4
+
5
+ import os
6
+ import urllib.request
7
+ from pathlib import Path
8
+ from .extractors import MediaExtractor, ProfilePictureExtractor
9
+ from .utils import (
10
+ validate_url, sanitize_filename, is_profile_url, is_media_url,
11
+ RateLimiter, ProgressBar, DownloadError, format_size, retry_on_failure
12
+ )
13
+
14
+
15
+ class InstagramDownloader:
16
+ """
17
+ Main Instagram downloader class
18
+ Supports: reels, posts (single/carousel images/videos), profile pictures
19
+ Public accounts only - no authentication required
20
+ """
21
+
22
+ BASE_HEADERS = {
23
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
24
+ 'Accept': '*/*',
25
+ 'Referer': 'https://www.instagram.com/',
26
+ }
27
+
28
+ def __init__(self, verbose=False, rate_limit=True):
29
+ """
30
+ Initialize downloader
31
+
32
+ Args:
33
+ verbose: Enable verbose logging
34
+ rate_limit: Enable rate limiting (recommended)
35
+ """
36
+ self.verbose = verbose
37
+ self.rate_limiter = RateLimiter(max_requests=30, time_window=60) if rate_limit else None
38
+
39
+ # Initialize extractors
40
+ self.media_extractor = MediaExtractor(verbose=verbose)
41
+ self.profile_extractor = ProfilePictureExtractor(verbose=verbose)
42
+
43
+ def log(self, message):
44
+ """Print verbose log messages"""
45
+ if self.verbose:
46
+ print(f"[parth-dl] {message}")
47
+
48
+ def get_info(self, url):
49
+ """
50
+ Get media information without downloading
51
+
52
+ Args:
53
+ url: Instagram URL (post, reel, or profile)
54
+
55
+ Returns:
56
+ Dictionary with media information
57
+ """
58
+ # Validate URL
59
+ validate_url(url)
60
+
61
+ # Rate limiting
62
+ if self.rate_limiter:
63
+ self.rate_limiter.wait_if_needed()
64
+
65
+ # Determine content type and extract
66
+ if is_profile_url(url):
67
+ self.log("Detected profile URL")
68
+ return self.profile_extractor.extract(url)
69
+ elif is_media_url(url):
70
+ self.log("Detected media URL")
71
+ return self.media_extractor.extract(url)
72
+ else:
73
+ raise DownloadError("Unsupported URL format. Use post/reel/profile URL.")
74
+
75
+ @retry_on_failure(max_retries=3)
76
+ def _download_file(self, url, output_path, show_progress=True):
77
+ """
78
+ Download file with progress bar
79
+
80
+ Args:
81
+ url: Direct download URL
82
+ output_path: Output file path
83
+ show_progress: Show download progress
84
+ """
85
+ req = urllib.request.Request(url, headers=self.BASE_HEADERS)
86
+
87
+ with urllib.request.urlopen(req, timeout=60) as response:
88
+ total_size = int(response.headers.get('Content-Length', 0))
89
+
90
+ # Create progress bar
91
+ progress = None
92
+ if show_progress and total_size > 0:
93
+ progress = ProgressBar(total_size)
94
+
95
+ # Download
96
+ with open(output_path, 'wb') as f:
97
+ while True:
98
+ chunk = response.read(8192)
99
+ if not chunk:
100
+ break
101
+
102
+ f.write(chunk)
103
+ if progress:
104
+ progress.update(len(chunk))
105
+
106
+ if progress:
107
+ progress.finish()
108
+
109
+ self.log(f"Downloaded: {output_path} ({format_size(os.path.getsize(output_path))})")
110
+
111
+ def _select_best_format(self, formats, quality='best'):
112
+ """
113
+ Select best format based on quality preference
114
+
115
+ Args:
116
+ formats: List of format dictionaries
117
+ quality: 'best' or 'worst'
118
+
119
+ Returns:
120
+ Selected format dictionary
121
+ """
122
+ if not formats:
123
+ return None
124
+
125
+ # Filter to only formats with audio (for videos)
126
+ video_formats = [f for f in formats if f.get('has_audio')]
127
+ if video_formats:
128
+ formats = video_formats
129
+
130
+ if quality == 'best':
131
+ # Best: highest resolution
132
+ return max(formats, key=lambda f: (
133
+ f.get('height', 0) * f.get('width', 0),
134
+ f.get('height', 0)
135
+ ))
136
+ else:
137
+ # Worst: lowest resolution
138
+ return min(formats, key=lambda f: (
139
+ f.get('height', 999999),
140
+ f.get('width', 999999)
141
+ ))
142
+
143
+ def download(self, url, output_path=None, quality='best'):
144
+ """
145
+ Download media from Instagram URL
146
+
147
+ Args:
148
+ url: Instagram URL (post, reel, or profile)
149
+ output_path: Output file/directory path (auto-generated if None)
150
+ quality: 'best' or 'worst'
151
+
152
+ Returns:
153
+ Path(s) to downloaded file(s)
154
+ """
155
+ # Get media info
156
+ info = self.get_info(url)
157
+
158
+ if not info:
159
+ raise DownloadError("Failed to extract media information")
160
+
161
+ # Prepare output directory
162
+ if output_path and os.path.isdir(output_path):
163
+ output_dir = output_path
164
+ output_path = None
165
+ else:
166
+ output_dir = os.getcwd()
167
+
168
+ # Handle different media types
169
+ media_type = info.get('type')
170
+ downloaded_files = []
171
+
172
+ print(f"\n{'='*70}")
173
+ print(f"Title: {info.get('title', 'Untitled')}")
174
+ print(f"Uploader: @{info.get('uploader', 'unknown')}")
175
+ print(f"Type: {media_type}")
176
+ print(f"{'='*70}\n")
177
+
178
+ # Download videos
179
+ if info.get('formats'):
180
+ selected_format = self._select_best_format(info['formats'], quality)
181
+
182
+ if not selected_format:
183
+ raise DownloadError("No suitable video format found")
184
+
185
+ # Generate filename
186
+ if not output_path:
187
+ safe_title = sanitize_filename(info.get('title', 'video'))
188
+ media_id = info.get('id', 'unknown')
189
+ output_path = os.path.join(output_dir, f"{safe_title}_{media_id}.mp4")
190
+
191
+ print(f"Resolution: {selected_format.get('width')}x{selected_format.get('height')}")
192
+ print(f"Audio: {'✓ YES' if selected_format.get('has_audio') else '✗ NO'}")
193
+ print(f"Output: {output_path}\n")
194
+
195
+ self._download_file(selected_format['url'], output_path)
196
+ downloaded_files.append(output_path)
197
+
198
+ # Download images
199
+ elif info.get('images'):
200
+ images = info['images']
201
+
202
+ if len(images) == 1:
203
+ # Single image
204
+ if not output_path:
205
+ safe_title = sanitize_filename(info.get('title', 'image'))
206
+ media_id = info.get('id', 'unknown')
207
+ output_path = os.path.join(output_dir, f"{safe_title}_{media_id}.jpg")
208
+
209
+ print(f"Output: {output_path}\n")
210
+ self._download_file(images[0]['url'], output_path)
211
+ downloaded_files.append(output_path)
212
+
213
+ else:
214
+ # Multiple images (carousel)
215
+ print(f"Downloading {len(images)} images from carousel...\n")
216
+
217
+ safe_title = sanitize_filename(info.get('title', 'carousel'))
218
+ media_id = info.get('id', 'unknown')
219
+
220
+ for idx, image in enumerate(images, 1):
221
+ filename = f"{safe_title}_{media_id}_{idx:02d}.jpg"
222
+ file_path = os.path.join(output_dir, filename)
223
+
224
+ print(f"[{idx}/{len(images)}] {filename}")
225
+ self._download_file(image['url'], file_path, show_progress=False)
226
+ downloaded_files.append(file_path)
227
+ print()
228
+
229
+ else:
230
+ raise DownloadError("No downloadable content found")
231
+
232
+ # Success message
233
+ print(f"{'='*70}")
234
+ print(f"✓ Download complete!")
235
+ print(f"Files saved: {len(downloaded_files)}")
236
+ for file in downloaded_files:
237
+ print(f" - {file}")
238
+ print(f"{'='*70}\n")
239
+
240
+ return downloaded_files[0] if len(downloaded_files) == 1 else downloaded_files
241
+
242
+ def list_formats(self, url):
243
+ """
244
+ List all available formats for a URL
245
+
246
+ Args:
247
+ url: Instagram URL
248
+ """
249
+ info = self.get_info(url)
250
+
251
+ print(f"\nMedia: {info.get('title', 'Untitled')}")
252
+ print(f"Uploader: @{info.get('uploader', 'unknown')}")
253
+ print(f"Type: {info.get('type', 'unknown')}")
254
+ print(f"{'='*70}\n")
255
+
256
+ # Video formats
257
+ if info.get('formats'):
258
+ print("Video Formats:")
259
+ for fmt in info['formats']:
260
+ audio = "🔊 WITH AUDIO" if fmt.get('has_audio') else "🔇 NO AUDIO"
261
+ print(f" {fmt.get('format_id')}: {fmt.get('width')}x{fmt.get('height')} [{audio}]")
262
+ print()
263
+
264
+ # Image formats
265
+ if info.get('images'):
266
+ print(f"Images: {len(info['images'])} image(s)")
267
+ for idx, img in enumerate(info['images'], 1):
268
+ print(f" [{idx}] {img.get('width')}x{img.get('height')}")
269
+ print()
270
+
271
+ # Thumbnail
272
+ if info.get('thumbnail'):
273
+ print(f"Thumbnail: {info['thumbnail'][:60]}...")
274
+ print()