Spaces:
Sleeping
Sleeping
File size: 6,897 Bytes
bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bc35cfe bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 93118a7 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 adc6b45 bf4a976 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | import asyncio
import logging
import os
import shutil
from pathlib import Path
from urllib.parse import urlparse
import aiofiles
import httpx
from dotenv import load_dotenv
from telegram import Update
from telegram.constants import ChatAction
from telegram.ext import (
Application,
CommandHandler,
ContextTypes,
MessageHandler,
filters,
)
load_dotenv()
BOT_TOKEN = os.environ["BOT_TOKEN"]
# Point strictly to localhost on port 7860 where the local server co-exists
BOT_API_URL = os.environ.get("BOT_API_URL", "http://127.0.0.1:7860/bot")
BOT_API_FILE_URL = os.environ.get("BOT_API_FILE_URL", "http://127.0.0.1:7860/file/bot")
DOWNLOAD_DIR = Path("/app/downloads")
DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
# Limit concurrent operations to save container resources
CONCURRENCY = int(os.environ.get("CONCURRENCY", "3"))
sem = asyncio.Semaphore(CONCURRENCY)
logging.basicConfig(
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
level=logging.INFO,
)
log = logging.getLogger("media-bot")
MEDIA_EXTS = {
"photo": {".jpg", ".jpeg", ".png", ".webp"},
"video": {".mp4", ".mkv", ".mov", ".webm", ".avi"},
"audio": {".mp3", ".m4a", ".ogg", ".wav", ".flac", ".opus"},
}
async def cmd_start(update: Update, _):
await update.message.reply_text(
"👋 *Media Downloader Bot*\n\n"
"Send me a link from Instagram, TikTok, YouTube, X, Reddit, Facebook... "
"or a direct file URL. I will fetch it and send it back to you "
"(supports up to 2 GB).",
parse_mode="Markdown",
)
def looks_like_direct_file(url: str) -> bool:
path = urlparse(url).path.lower()
return any(path.endswith(ext) for cat in MEDIA_EXTS.values() for ext in cat)
async def fetch_direct(url: str, dest: Path):
"""Fast async streaming for direct URLs bypassing yt-dlp overhead."""
async with httpx.AsyncClient(follow_redirects=True, timeout=httpx.Timeout(600.0)) as client:
async with client.stream("GET", url) as response:
response.raise_for_status()
async with aiofiles.open(dest, "wb") as f:
async for chunk in response.aiter_bytes(1 << 20): # 1 MiB chunks
await f.write(chunk)
async def fetch_with_ytdlp(url: str, dest_dir: Path) -> list[Path]:
"""Executes yt-dlp in a managed subprocess with multi-threading optimized commands."""
out_template = str(dest_dir / "%(title).200B.%(ext)s")
cmd = [
"yt-dlp",
"--no-warnings",
"--no-playlist",
"--no-progress",
"--no-mtime",
"--concurrent-fragments", "4", # Parallel fragment downloads for speed
"--retries", "5",
"--embed-metadata",
"--embed-thumbnail",
"--merge-output-format", "mp4",
"--format", "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best",
"--output", out_template,
]
# Automatically check and inject cookies if available
cookies_file = Path("/app/cookies.txt")
if cookies_file.exists() and cookies_file.stat().st_size > 0:
cmd.extend(["--cookies", str(cookies_file)])
log.info("Using exported cookies for extraction.")
cmd.append(url)
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
_, stderr = await proc.communicate()
if proc.returncode != 0:
raise RuntimeError(stderr.decode(errors="replace").strip()[:1500])
return [p for p in dest_dir.iterdir() if p.is_file()]
def classify(path: Path):
ext = path.suffix.lower()
for kind, exts in MEDIA_EXTS.items():
if ext in exts:
return kind
return "document"
async def send_file(update: Update, path: Path):
kind = classify(path)
caption = path.name
with path.open("rb") as fh:
if kind == "photo":
await update.message.reply_photo(photo=fh, caption=caption)
elif kind == "video":
await update.message.reply_video(video=fh, caption=caption, supports_streaming=True)
elif kind == "audio":
await update.message.reply_audio(audio=fh, caption=caption)
else:
await update.message.reply_document(document=fh, caption=caption)
async def handle_link(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
url = update.message.text.strip()
user = update.message.from_user
log.info("User %s requested URL: %s", user.id, url)
status = await update.message.reply_text("⏳ Downloading... Please wait.")
async with sem:
tmp_dir = DOWNLOAD_DIR / f"{update.update_id}"
tmp_dir.mkdir(parents=True, exist_ok=True)
try:
# Keeps the interactive "uploading document" continuous during high latency
ping_task = asyncio.create_task(_keep_pinging(update))
try:
if looks_like_direct_file(url):
name = Path(urlparse(url).path).name or "file"
dest = tmp_dir / name
await fetch_direct(url, dest)
files = [dest]
else:
files = await fetch_with_ytdlp(url, tmp_dir)
finally:
ping_task.cancel()
if not files:
await status.edit_text("⚠️ No downloadable media found.")
return
await status.edit_text(f"📤 Uploading {len(files)} file(s) to Telegram...")
await update.message.chat.send_action(ChatAction.UPLOAD_DOCUMENT)
for f in files:
await send_file(update, f)
await status.delete()
except Exception as e:
log.exception("Task failed.")
await status.edit_text(f"❌ Error: {e}"[:4000])
finally:
shutil.rmtree(tmp_dir, ignore_errors=True)
async def _keep_pinging(update: Update):
"""Refreshes Telegram status action every 4 seconds for huge transfers."""
try:
while True:
await update.message.chat.send_action(ChatAction.UPLOAD_DOCUMENT)
await asyncio.sleep(4)
except asyncio.CancelledError:
pass
def build_app() -> Application:
return (
Application.builder()
.token(BOT_TOKEN)
.base_url(BOT_API_URL)
.base_file_url(BOT_API_FILE_URL)
.local_mode(True) # Activates Local Server Mode for 2 GB files
.read_timeout(600)
.write_timeout(600)
.connect_timeout(60)
.pool_timeout(60)
.build()
)
def main():
app = build_app()
app.add_handler(CommandHandler("start", cmd_start))
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_link))
log.info("Bot infrastructure successfully initialized.")
app.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()
|