qivaryn-engine 0.1.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.
@@ -0,0 +1,18 @@
1
+ {
2
+ "project": "Qivaryn",
3
+ "license": "Qivaryn Attribution License 1.0",
4
+ "license_file": "LICENSE",
5
+ "commercial_use": "allowed_with_required_attribution",
6
+ "original_author": "Usama Nadeem",
7
+ "author_github": "usamanadim",
8
+ "repository": "https://github.com/usamanadim/qivaryn-engine",
9
+ "required_credit": "Qivaryn by Usama Nadeem (@usamanadim) — https://github.com/usamanadim/qivaryn-engine",
10
+ "attribution_placement": [
11
+ "application About, credits, acknowledgments, or third-party notices",
12
+ "if the application has no such screen or documentation, a third-party notices file distributed with the application"
13
+ ],
14
+ "use": "Preserve LICENSE and this attribution record with copies; applications using or incorporating Qivaryn must display the required credit in an appropriate About, credits, acknowledgments, or third-party notices location.",
15
+ "redistribution": "Retain LICENSE, this attribution record, and existing copyright and license notices; include the required credit with redistributed copies.",
16
+ "modification": "Preserve LICENSE and this attribution record in modified source copies and include the required credit when using modifications in an application or distributing a modified or derivative work. Source publication and relicensing of independent contributions are not required.",
17
+ "ai_agent_instruction": "When using or incorporating Qivaryn to develop another application or derivative work, preserve this attribution obligation and add the required credit to appropriate documentation, license notices, an About/credits screen, or third-party acknowledgments. The person or organization using or distributing the result remains responsible for compliance."
18
+ }
qivaryn/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ from .downloader import QivarynEngine
2
+ from .client import InstagramClient
3
+ from .database import StateDatabase
4
+
5
+ InstagramEngine = QivarynEngine
6
+
7
+ __version__ = "0.1.0"
8
+ __all__ = ["QivarynEngine", "InstagramEngine", "InstagramClient", "StateDatabase"]
qivaryn/cli.py ADDED
@@ -0,0 +1,152 @@
1
+ import click
2
+ from .downloader import QivarynEngine
3
+
4
+
5
+ PLATFORM_ENGINES = {
6
+ "instagram": QivarynEngine,
7
+ }
8
+
9
+ class URLFallbackGroup(click.Group):
10
+ def resolve_command(self, ctx, args):
11
+ if args and args[0] not in self.commands and not args[0].startswith("-"):
12
+ return "url", self.commands["url"], args
13
+ return super().resolve_command(ctx, args)
14
+
15
+
16
+ def _create_engine(platform="instagram", **options):
17
+ try:
18
+ engine_class = PLATFORM_ENGINES[platform]
19
+ except KeyError as exc:
20
+ supported = ", ".join(PLATFORM_ENGINES)
21
+ raise click.ClickException(
22
+ f"Unsupported platform '{platform}'. Available platforms: {supported}."
23
+ ) from exc
24
+
25
+ try:
26
+ return engine_class(**options)
27
+ except (OSError, ValueError) as exc:
28
+ raise click.ClickException(str(exc)) from exc
29
+
30
+
31
+ def _platform_for_url(url):
32
+ for platform, engine_class in PLATFORM_ENGINES.items():
33
+ try:
34
+ detected = engine_class.detect_url(url)
35
+ except ValueError as exc:
36
+ raise click.ClickException(f"Invalid URL: {exc}") from exc
37
+ if detected is not None:
38
+ return platform
39
+ raise click.ClickException(
40
+ "Could not identify a supported platform or content type from this URL."
41
+ )
42
+
43
+
44
+ @click.group(cls=URLFallbackGroup)
45
+ def cli():
46
+ """Qivaryn media downloader. Pass a URL directly for automatic detection."""
47
+ pass
48
+
49
+ @cli.command()
50
+ @click.argument("url_or_shortcode")
51
+ @click.option("--platform", type=click.Choice(tuple(PLATFORM_ENGINES)), default="instagram", show_default=True, help="Media platform to use.")
52
+ @click.option("--cookies", "--cookies-file", "cookies_file", type=click.Path(dir_okay=False), help="Path to Netscape cookies.txt file exported from browser.")
53
+ @click.option("--sessionid", help="Direct sessionid cookie string.")
54
+ @click.option("--output", "-o", default="./downloads", help="Directory to save downloaded media.")
55
+ @click.option("--db", default="qivaryn_state.db", help="Path to SQLite state tracking database.")
56
+ @click.option("--force", "-f", is_flag=True, default=False, help="Force re-download even if already downloaded.")
57
+ def single(url_or_shortcode, platform, cookies_file, sessionid, output, db, force):
58
+ """Download a single Reel, Post, or Carousel by URL or shortcode."""
59
+ engine = _create_engine(platform=platform, output_dir=output, db_path=db, sessionid=sessionid, cookies_file=cookies_file)
60
+ engine.download_single(url_or_shortcode, force=force)
61
+
62
+ @cli.command()
63
+ @click.argument("username")
64
+ @click.option("--platform", type=click.Choice(tuple(PLATFORM_ENGINES)), default="instagram", show_default=True, help="Media platform to use.")
65
+ @click.option("--cookies", "--cookies-file", "cookies_file", type=click.Path(dir_okay=False), help="Path to Netscape cookies.txt file exported from browser.")
66
+ @click.option("--sessionid", help="Direct sessionid cookie string.")
67
+ @click.option("--type", "media_type", type=click.Choice(["all", "posts", "reels", "videos", "carousels"], case_sensitive=False), default="all", help="Filter media: 'all', 'posts', 'reels', 'videos', or 'carousels'.")
68
+ @click.option("--count", "-n", "count", type=int, default=None, help="Number of new items to download (e.g. 100). Automatically skips existing content and stops once this number of new items is saved.")
69
+ @click.option("--limit", "-l", "limit", type=int, default=None, help="Alias for --count.")
70
+ @click.option("--output", "-o", default="./downloads", help="Directory to save downloaded media.")
71
+ @click.option("--db", default="qivaryn_state.db", help="Path to SQLite state tracking database.")
72
+ @click.option("--threshold", default=3, help="Consecutive known posts before early-exit halt (only active when --count is omitted).")
73
+ @click.option("--max-pages", type=int, default=None, help="Maximum pagination pages to scan.")
74
+ @click.option("--force", "-f", is_flag=True, default=False, help="Force re-download even if already downloaded.")
75
+ def download(username, platform, cookies_file, sessionid, media_type, count, limit, output, db, threshold, max_pages, force):
76
+ """Download account content with media filtering, batch count limits, and fast resumption."""
77
+ target_count = count if count is not None else limit
78
+ if target_count is not None and target_count <= 0:
79
+ raise click.BadParameter("Count must be a positive integer greater than 0.", param_hint="--count")
80
+
81
+ engine = _create_engine(
82
+ platform=platform,
83
+ output_dir=output,
84
+ db_path=db,
85
+ sessionid=sessionid,
86
+ cookies_file=cookies_file,
87
+ consecutive_threshold=threshold
88
+ )
89
+ engine.download_account(username, media_type_filter=media_type, max_pages=max_pages, force=force, count=target_count)
90
+
91
+ @cli.command("url", hidden=True)
92
+ @click.argument("url")
93
+ @click.option("--type", "media_type", type=click.Choice(["all", "posts", "reels", "videos", "carousels"], case_sensitive=False), default=None, help="Filter media: 'all', 'posts', 'reels', 'videos', or 'carousels'.")
94
+ @click.option("--cookies", "--cookies-file", "cookies_file", type=click.Path(dir_okay=False), help="Path to Netscape cookies.txt file exported from browser.")
95
+ @click.option("--sessionid", help="Direct sessionid cookie string.")
96
+ @click.option("--count", "-n", "count", type=int, default=None, help="Number of new items to download from a profile or Reels tab.")
97
+ @click.option("--limit", "-l", "limit", type=int, default=None, help="Alias for --count.")
98
+ @click.option("--output", "-o", default="./downloads", help="Directory to save downloaded media.")
99
+ @click.option("--db", default="qivaryn_state.db", help="Path to SQLite state tracking database.")
100
+ @click.option("--max-pages", type=int, default=None, help="Maximum pagination pages to scan per feed.")
101
+ @click.option("--force", "-f", is_flag=True, default=False, help="Force re-download even if already downloaded.")
102
+ def download_url(url, media_type, cookies_file, sessionid, count, limit, output, db, max_pages, force):
103
+ """Automatically download content based on an Instagram URL."""
104
+ target_count = count if count is not None else limit
105
+ if target_count is not None and target_count <= 0:
106
+ raise click.BadParameter("Count must be a positive integer greater than 0.", param_hint="--count")
107
+
108
+ platform = _platform_for_url(url)
109
+ engine = _create_engine(
110
+ platform=platform,
111
+ output_dir=output,
112
+ db_path=db,
113
+ sessionid=sessionid,
114
+ cookies_file=cookies_file,
115
+ )
116
+ if media_type:
117
+ engine.download_url(url, max_pages=max_pages, force=force, count=target_count, media_type=media_type)
118
+ else:
119
+ engine.download_url(url, max_pages=max_pages, force=force, count=target_count)
120
+
121
+ @cli.command("clear-db")
122
+ @click.argument("item", required=False, default=None)
123
+ @click.option("--platform", type=click.Choice(tuple(PLATFORM_ENGINES)), default="instagram", show_default=True, help="Media platform whose history to manage.")
124
+ @click.option("--item", "-i", "item_opt", help="Specific shortcode or post URL to remove from database.")
125
+ @click.option("--all", "-a", "clear_all_flag", is_flag=True, default=False, help="Clear entire download history from database.")
126
+ @click.option("--missing", "-m", "--prune", is_flag=True, default=False, help="Remove records whose media files were deleted from disk.")
127
+ @click.option("--output", "-o", default="./downloads", help="Directory where media files are stored.")
128
+ @click.option("--db", default="qivaryn_state.db", help="Path to SQLite state tracking database.")
129
+ @click.option("--yes", "-y", is_flag=True, default=False, help="Confirm clearing all records without prompting.")
130
+ def clear_db(item, platform, item_opt, clear_all_flag, missing, output, db, yes):
131
+ """Remove download records from database without deleting media files."""
132
+ target_item = item or item_opt
133
+ engine = _create_engine(platform=platform, output_dir=output, db_path=db)
134
+
135
+ if target_item:
136
+ engine.clear_database(item=target_item)
137
+ elif missing:
138
+ engine.clear_database(prune_missing=True)
139
+ elif clear_all_flag or yes:
140
+ engine.clear_database(clear_all=True)
141
+ else:
142
+ record_count = engine.db.get_record_count()
143
+ if record_count == 0:
144
+ click.echo("[*] Database is already empty (0 records).")
145
+ return
146
+ if click.confirm(f"Are you sure you want to clear all {record_count} download records? (Existing files will NOT be deleted)"):
147
+ engine.clear_database(clear_all=True)
148
+ else:
149
+ click.echo("[-] Operation cancelled.")
150
+
151
+ if __name__ == "__main__":
152
+ cli()