Files
plain-ub-overfork/app/plugins/files/torrent_leech.py
overspend1 8fe355ed0c Update README and alive command for @overspend1 fork
- Updated README title to show OVERSPEND1 FORK
- Changed maintainer credit to @overspend1
- Updated alive command to show @overspend1 as creator instead of Meliodas
2025-07-25 20:27:05 +02:00

406 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import asyncio
import os
import time
import tempfile
import hashlib
from pathlib import Path
from datetime import datetime
from typing import Optional, Dict, List
import aiohttp
from ub_core.utils import progress
from app import BOT, Message, bot
# Try to import libtorrent, make it optional
try:
import libtorrent as lt
LIBTORRENT_AVAILABLE = True
except ImportError:
LIBTORRENT_AVAILABLE = False
lt = None
# Import pixeldrain functionality
try:
from .pixeldrain import pixeldrain
PIXELDRAIN_AVAILABLE = True
except ImportError:
PIXELDRAIN_AVAILABLE = False
class TorrentLeecher:
def __init__(self):
self.session = lt.session()
self.session.listen_on(6881, 6891)
self.active_torrents: Dict[str, Dict] = {}
self.download_dir = Path("downloads/torrents")
self.download_dir.mkdir(parents=True, exist_ok=True)
def add_torrent(self, torrent_data: bytes, download_path: Path) -> str:
"""Add torrent to session and return info hash"""
if not LIBTORRENT_AVAILABLE:
raise Exception("libtorrent not available - install python-libtorrent")
info = lt.torrent_info(torrent_data)
handle = self.session.add_torrent({
'ti': info,
'save_path': str(download_path.parent),
'storage_mode': lt.storage_mode_t.storage_mode_sparse,
})
info_hash = str(info.info_hash())
self.active_torrents[info_hash] = {
'handle': handle,
'info': info,
'start_time': time.time(),
'download_path': download_path,
'completed': False
}
return info_hash
def get_torrent_status(self, info_hash: str) -> Optional[Dict]:
"""Get status of a torrent"""
if info_hash not in self.active_torrents:
return None
handle = self.active_torrents[info_hash]['handle']
status = handle.status()
return {
'name': self.active_torrents[info_hash]['info'].name(),
'progress': status.progress,
'download_rate': status.download_rate,
'upload_rate': status.upload_rate,
'num_peers': status.num_peers,
'num_seeds': status.num_seeds,
'total_size': self.active_torrents[info_hash]['info'].total_size(),
'downloaded': status.total_done,
'eta': self._calculate_eta(status),
'state': str(status.state),
'completed': status.is_finished
}
def _calculate_eta(self, status) -> str:
"""Calculate estimated time of arrival"""
if status.download_rate <= 0:
return ""
remaining = status.total_wanted - status.total_done
eta_seconds = remaining / status.download_rate
if eta_seconds < 60:
return f"{int(eta_seconds)}s"
elif eta_seconds < 3600:
return f"{int(eta_seconds/60)}m"
else:
return f"{int(eta_seconds/3600)}h {int((eta_seconds%3600)/60)}m"
async def download_torrent(self, info_hash: str, progress_callback=None) -> List[Path]:
"""Download torrent and return list of downloaded files"""
if info_hash not in self.active_torrents:
raise Exception("Torrent not found")
handle = self.active_torrents[info_hash]['handle']
# Wait for torrent to complete
while not handle.status().is_finished:
status = self.get_torrent_status(info_hash)
if progress_callback:
await progress_callback(status)
await asyncio.sleep(2)
# Get list of downloaded files
info = self.active_torrents[info_hash]['info']
base_path = self.active_torrents[info_hash]['download_path']
downloaded_files = []
for i in range(info.num_files()):
file_info = info.file_at(i)
file_path = base_path / file_info.path
if file_path.exists():
downloaded_files.append(file_path)
self.active_torrents[info_hash]['completed'] = True
return downloaded_files
def remove_torrent(self, info_hash: str, delete_files: bool = False):
"""Remove torrent from session"""
if info_hash in self.active_torrents:
handle = self.active_torrents[info_hash]['handle']
if delete_files:
self.session.remove_torrent(handle, lt.options_t.delete_files)
else:
self.session.remove_torrent(handle)
del self.active_torrents[info_hash]
torrent_leecher = TorrentLeecher()
async def download_torrent_file(url: str) -> bytes:
"""Download torrent file from URL"""
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
if response.status == 200:
return await response.read()
else:
raise Exception(f"Failed to download torrent: {response.status}")
def parse_magnet_link(magnet_url: str) -> bytes:
"""Convert magnet link to torrent data (simplified)"""
# This is a basic implementation - in practice, you'd need to resolve the magnet link
# For now, we'll raise an exception asking for .torrent file
raise Exception("Magnet links not supported yet. Please provide a .torrent file URL.")
@bot.add_cmd(cmd="torrent")
async def torrent_leech(bot: BOT, message: Message):
"""
CMD: TORRENT
INFO: Download torrents and optionally upload to PixelDrain (requires libtorrent)
FLAGS:
-pd: Upload to PixelDrain after download
-del: Delete local files after upload
USAGE:
.torrent [torrent_url]
.torrent -pd [torrent_url]
.torrent -pd -del [torrent_url]
"""
response = await message.reply("🔄 <b>Processing torrent request...</b>")
if not LIBTORRENT_AVAILABLE:
await response.edit("❌ <b>LibTorrent not available!</b>\n\n"
"Install python-libtorrent to use torrent functionality.\n"
"Use <code>.leech</code> command instead for aria2c-based downloads.")
return
if not message.input:
await response.edit("❌ <b>No torrent URL provided!</b>\n\n"
"<b>Usage:</b>\n"
"• <code>.torrent [torrent_url]</code>\n"
"• <code>.torrent -pd [torrent_url]</code> (upload to PixelDrain)\n"
"• <code>.torrent -pd -del [torrent_url]</code> (upload and delete local)")
return
torrent_url = message.filtered_input.strip()
upload_to_pixeldrain = "-pd" in message.flags
delete_after_upload = "-del" in message.flags
if upload_to_pixeldrain and not PIXELDRAIN_AVAILABLE:
await response.edit("❌ <b>PixelDrain module not available!</b>\n"
"Cannot upload to PixelDrain.")
return
try:
# Download torrent file
await response.edit("📥 <b>Downloading torrent file...</b>")
if torrent_url.startswith('magnet:'):
try:
torrent_data = parse_magnet_link(torrent_url)
except Exception as e:
await response.edit(f"❌ <b>Magnet link error:</b>\n<code>{str(e)}</code>")
return
else:
torrent_data = await download_torrent_file(torrent_url)
# Create download directory
torrent_hash = hashlib.sha1(torrent_data).hexdigest()[:10]
download_path = torrent_leecher.download_dir / f"torrent_{torrent_hash}_{int(time.time())}"
download_path.mkdir(parents=True, exist_ok=True)
# Add torrent to session
await response.edit(" <b>Adding torrent to session...</b>")
info_hash = torrent_leecher.add_torrent(torrent_data, download_path)
# Progress tracking function
async def progress_callback(status):
progress_text = f"📊 <b>Downloading Torrent</b>\n\n"
progress_text += f"📁 <b>Name:</b> <code>{status['name']}</code>\n"
progress_text += f"📈 <b>Progress:</b> {status['progress']:.1%}\n"
progress_text += f"⬇️ <b>Speed:</b> {status['download_rate'] / 1024 / 1024:.1f} MB/s\n"
progress_text += f"⬆️ <b>Upload:</b> {status['upload_rate'] / 1024 / 1024:.1f} MB/s\n"
progress_text += f"👥 <b>Peers:</b> {status['num_peers']} ({status['num_seeds']} seeds)\n"
progress_text += f"📊 <b>Size:</b> {status['downloaded'] / 1024 / 1024:.1f}/{status['total_size'] / 1024 / 1024:.1f} MB\n"
progress_text += f"⏱ <b>ETA:</b> {status['eta']}\n"
progress_text += f"🔄 <b>State:</b> {status['state']}"
try:
await response.edit(progress_text)
except:
pass # Ignore edit errors due to rate limiting
# Start download
await response.edit("🚀 <b>Starting torrent download...</b>")
downloaded_files = await torrent_leecher.download_torrent(info_hash, progress_callback)
if not downloaded_files:
await response.edit("❌ <b>No files were downloaded!</b>")
return
# Upload to PixelDrain if requested
uploaded_links = []
if upload_to_pixeldrain:
await response.edit("📤 <b>Uploading files to PixelDrain...</b>")
for file_path in downloaded_files:
try:
file_info = await pixeldrain.upload_file(file_path, response)
uploaded_links.append({
'name': file_path.name,
'url': file_info['url'],
'size': file_info['size']
})
# Delete local file if requested
if delete_after_upload and file_path.exists():
file_path.unlink()
except Exception as e:
uploaded_links.append({
'name': file_path.name,
'error': str(e)
})
# Clean up torrent from session
torrent_leecher.remove_torrent(info_hash, delete_files=delete_after_upload)
# Format final response
result_text = f"✅ <b>Torrent download completed!</b>\n\n"
torrent_status = torrent_leecher.get_torrent_status(info_hash)
if torrent_status:
result_text += f"📁 <b>Name:</b> <code>{torrent_status['name']}</code>\n"
result_text += f"📊 <b>Total Size:</b> {torrent_status['total_size'] / 1024 / 1024:.1f} MB\n"
result_text += f"📂 <b>Files Downloaded:</b> {len(downloaded_files)}\n\n"
if upload_to_pixeldrain:
result_text += f"🔗 <b>PixelDrain Links:</b>\n"
for link_info in uploaded_links:
if 'error' in link_info:
result_text += f"❌ <code>{link_info['name']}</code>: {link_info['error']}\n"
else:
size_mb = link_info['size'] / 1024 / 1024
result_text += f"✅ <code>{link_info['name']}</code> ({size_mb:.1f} MB)\n"
result_text += f" {link_info['url']}\n\n"
else:
result_text += f"📍 <b>Local Path:</b>\n<code>{download_path}</code>\n\n"
result_text += f"📋 <b>Files:</b>\n"
for file_path in downloaded_files[:10]: # Show max 10 files
size_mb = file_path.stat().st_size / 1024 / 1024
result_text += f"• <code>{file_path.name}</code> ({size_mb:.1f} MB)\n"
if len(downloaded_files) > 10:
result_text += f"... and {len(downloaded_files) - 10} more files"
await response.edit(result_text)
except Exception as e:
await response.edit(f"❌ <b>Torrent download failed!</b>\n\n<code>{str(e)}</code>")
# Clean up on error
if 'info_hash' in locals():
torrent_leecher.remove_torrent(info_hash, delete_files=True)
@bot.add_cmd(cmd="torrentlist")
async def torrent_list(bot: BOT, message: Message):
"""
CMD: TORRENTLIST
INFO: List active torrents
USAGE: .torrentlist
"""
if not torrent_leecher.active_torrents:
await message.reply("📭 <b>No active torrents</b>")
return
list_text = f"📋 <b>Active Torrents ({len(torrent_leecher.active_torrents)})</b>\n\n"
for info_hash, torrent_data in torrent_leecher.active_torrents.items():
status = torrent_leecher.get_torrent_status(info_hash)
if status:
elapsed = int(time.time() - torrent_data['start_time'])
elapsed_str = f"{elapsed//3600}h {(elapsed%3600)//60}m" if elapsed > 3600 else f"{elapsed//60}m {elapsed%60}s"
list_text += f"🔸 <b>{status['name'][:30]}...</b>\n"
list_text += f" 📈 Progress: {status['progress']:.1%}\n"
list_text += f" ⬇️ Speed: {status['download_rate'] / 1024 / 1024:.1f} MB/s\n"
list_text += f" ⏱ Runtime: {elapsed_str}\n"
list_text += f" 🆔 Hash: <code>{info_hash[:12]}...</code>\n\n"
await message.reply(list_text)
@bot.add_cmd(cmd="torrentstop")
async def torrent_stop(bot: BOT, message: Message):
"""
CMD: TORRENTSTOP
INFO: Stop and remove a torrent
FLAGS: -del to delete downloaded files
USAGE:
.torrentstop [info_hash]
.torrentstop -del [info_hash]
"""
if not message.input:
await message.reply("❌ <b>No torrent hash provided!</b>\n\n"
"Use <code>.torrentlist</code> to see active torrents.")
return
info_hash = message.filtered_input.strip()
delete_files = "-del" in message.flags
if info_hash not in torrent_leecher.active_torrents:
await message.reply("❌ <b>Torrent not found!</b>\n\n"
"Use <code>.torrentlist</code> to see active torrents.")
return
try:
torrent_name = torrent_leecher.active_torrents[info_hash]['info'].name()
torrent_leecher.remove_torrent(info_hash, delete_files)
action = "stopped and files deleted" if delete_files else "stopped"
await message.reply(f"✅ <b>Torrent {action}!</b>\n\n"
f"📁 <b>Name:</b> <code>{torrent_name}</code>\n"
f"🆔 <b>Hash:</b> <code>{info_hash[:12]}...</code>")
except Exception as e:
await message.reply(f"❌ <b>Failed to stop torrent!</b>\n\n<code>{str(e)}</code>")
@bot.add_cmd(cmd="torrenthelp")
async def torrent_help(bot: BOT, message: Message):
"""
CMD: TORRENTHELP
INFO: Show torrent commands help
USAGE: .torrenthelp
"""
help_text = f"📋 <b>Torrent Leech Commands</b>\n\n"
help_text += f"🚀 <b>Download Commands:</b>\n"
help_text += f"• <code>.torrent [url]</code> - Download torrent\n"
help_text += f"• <code>.torrent -pd [url]</code> - Download and upload to PixelDrain\n"
help_text += f"• <code>.torrent -pd -del [url]</code> - Upload to PixelDrain and delete local\n\n"
help_text += f"📊 <b>Management Commands:</b>\n"
help_text += f"• <code>.torrentlist</code> - List active torrents\n"
help_text += f"• <code>.torrentstop [hash]</code> - Stop torrent\n"
help_text += f"• <code>.torrentstop -del [hash]</code> - Stop and delete files\n\n"
help_text += f"💡 <b>Usage Examples:</b>\n"
help_text += f"• <code>.torrent https://site.com/file.torrent</code>\n"
help_text += f"• <code>.torrent -pd https://site.com/movie.torrent</code>\n\n"
help_text += f"⚠️ <b>Notes:</b>\n"
help_text += f"• Only .torrent file URLs supported (no magnet links yet)\n"
help_text += f"• PixelDrain integration requires pixeldrain module\n"
help_text += f"• Downloaded files stored in downloads/torrents/\n"
help_text += f"• Use responsibly and respect copyright laws"
await message.reply(help_text)