Add Instagram Downloader Plugin for Ultroid#497
Add Instagram Downloader Plugin for Ultroid#497paman7647 wants to merge 2 commits intoTeamUltroid:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Adds an Instagram media downloader command intended to integrate as an Ultroid plugin, using yt-dlp to fetch and upload Instagram reels/videos/photos to Telegram.
Changes:
- Introduces a new Instagram downloader command (
.ig <link>) with progress reporting. - Implements download extraction via
yt-dlpand uploads results viafast_uploader+send_file.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| from pyUltroid import LOGS | ||
| from pyUltroid.fns.helper import humanbytes, run_async, time_formatter | ||
| from pyUltroid.fns.tools import set_attributes | ||
| from . import ultroid_cmd |
There was a problem hiding this comment.
This module is added at the repo root, but Ultroid’s Loader only auto-imports plugin modules from the plugins/ directory; additionally, from . import ultroid_cmd will fail unless the file is inside the plugins package. Please move/rename this to plugins/instagram.py (or similar) and keep imports consistent with other plugins so it’s actually loaded at startup.
| from . import ultroid_cmd | |
| from plugins import ultroid_cmd |
| opts = { | ||
| "quiet": True, | ||
| "prefer_ffmpeg": True, | ||
| "geo-bypass": True, | ||
| "nocheckcertificate": True, | ||
| "outtmpl": "%(id)s.%(ext)s", | ||
| "progress_hooks": [lambda d: asyncio.create_task(ig_progress(d, time.time(), msg))], |
There was a problem hiding this comment.
progress_hooks is invoked from the yt-dlp download thread (because _download_ig uses @run_async), so calling asyncio.create_task(...) here will raise RuntimeError: no running event loop and/or spawn an unbounded number of tasks. Capture the main loop once (e.g., loop = asyncio.get_running_loop() and a single start_time) and use a thread-safe scheduling approach (e.g., asyncio.run_coroutine_threadsafe) with proper rate-limiting.
| opts = { | |
| "quiet": True, | |
| "prefer_ffmpeg": True, | |
| "geo-bypass": True, | |
| "nocheckcertificate": True, | |
| "outtmpl": "%(id)s.%(ext)s", | |
| "progress_hooks": [lambda d: asyncio.create_task(ig_progress(d, time.time(), msg))], | |
| # Capture the main event loop and a single start time for this download session | |
| loop = asyncio.get_running_loop() | |
| start_time = time.time() | |
| # Simple rate-limiting state to avoid flooding the loop with tasks | |
| _rate_limit_state = {"last_update": 0.0} | |
| def progress_hook(d): | |
| """ | |
| Thread-safe progress hook called by yt-dlp from its download thread. | |
| Schedules ig_progress on the main asyncio loop with basic rate limiting. | |
| """ | |
| now = time.time() | |
| # Allow an update at most once per second | |
| if now - _rate_limit_state["last_update"] < 1.0: | |
| return | |
| _rate_limit_state["last_update"] = now | |
| try: | |
| asyncio.run_coroutine_threadsafe( | |
| ig_progress(d, start_time, msg), | |
| loop, | |
| ) | |
| except RuntimeError: | |
| # Event loop may be closed; ignore in that case | |
| pass | |
| opts = { | |
| "quiet": True, | |
| "prefer_ffmpeg": True, | |
| "geo-bypass": True, | |
| "nocheckcertificate": True, | |
| "outtmpl": "%(id)s.%(ext)s", | |
| "progress_hooks": [progress_hook], |
| media_path = None | ||
| for f in glob.glob(f"{media_id}*"): | ||
| if not f.endswith(".jpg"): | ||
| media_path = f | ||
| break | ||
|
|
||
| if not media_path: |
There was a problem hiding this comment.
This file-selection logic skips any downloaded .jpg file, which means Instagram photo posts (and carousels containing photos) will be silently ignored because the only downloaded media may be a .jpg. Instead of excluding .jpg, use yt-dlp’s returned filename fields (e.g., _filename/requested_downloads) or select by the entry’s expected extension so photos are handled correctly.
| media_path = None | |
| for f in glob.glob(f"{media_id}*"): | |
| if not f.endswith(".jpg"): | |
| media_path = f | |
| break | |
| if not media_path: | |
| media_path = media.get("_filename") | |
| # Fallback: construct from id/ext using outtmpl "%(id)s.%(ext)s" | |
| if not media_path: | |
| ext = media.get("ext") | |
| if ext: | |
| candidate = f"{media_id}.{ext}" | |
| if os.path.exists(candidate): | |
| media_path = candidate | |
| # Last resort: any file starting with the media_id (no .jpg exclusion) | |
| if not media_path: | |
| matches = glob.glob(f"{media_id}.*") | |
| if matches: | |
| media_path = matches[0] | |
| if not media_path or not os.path.exists(media_path): |
| title = media.get("title") or "Instagram_Media" | ||
|
|
||
| if len(title) > 30: | ||
| title = title[:27] + "..." | ||
|
|
||
| # Find downloaded file from yt-dlp | ||
| media_path = None | ||
| for f in glob.glob(f"{media_id}*"): | ||
| if not f.endswith(".jpg"): | ||
| media_path = f | ||
| break | ||
|
|
||
| if not media_path: | ||
| continue | ||
|
|
||
| # Rename file | ||
| ext = "." + media_path.split(".")[-1] | ||
| final_name = f"{title}{ext}" | ||
|
|
||
| try: |
There was a problem hiding this comment.
title comes from remote metadata and is used directly to build final_name. If the title contains path separators (e.g., ../, /, \) or other special characters, os.rename may write outside the working dir or fail unpredictably. Please sanitize to a safe basename (strip separators / reserved chars) before constructing the output filename.
No description provided.