From d973ac2cc4e345ad89dff967b96e43659c00c2d1 Mon Sep 17 00:00:00 2001 From: Hafitz Setya Date: Tue, 7 Sep 2021 12:47:04 +0700 Subject: [PATCH] Add back Torrent search Co-authored-by: Bipul Dey Co-authored-by: Juan Signed-off-by: breakdowns --- README.md | 12 +- bot/__main__.py | 9 +- bot/helper/custom_filters.py | 23 ++ bot/helper/telegram_helper/bot_commands.py | 1 + bot/modules/torrent_search.py | 339 +++++++++++++++++++++ 5 files changed, 379 insertions(+), 5 deletions(-) create mode 100644 bot/helper/custom_filters.py create mode 100644 bot/modules/torrent_search.py diff --git a/README.md b/README.md index acbaa19..f83884d 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,11 @@ - Clone status - Search in multiple Drive folder/TD - Many bugs has been fixed +- Torrent search Supported: +``` +nyaa.si, sukebei, 1337x, piratebay, +tgx, yts, eztv, torlock, rarbg +``` - Direct links Supported: ``` letsupload.io, hxfile.co, anonfiles.com, bayfiles.com, antfiles, @@ -169,7 +174,8 @@ Fill up rest of the fields. Meaning of each field is discussed below: - `SHORTENER_API`: Fill your Shortener API key if you are using Shortener. - `SHORTENER`: if you want to use Shortener in G-Drive and index link, fill Shortener URL here. Examples: ``` -exe.io, gplinks.in, shrinkme.io, urlshortx.com, shortzon.com +exe.io, gplinks.in, shrinkme.io, urlshortx.com, shortzon.com, +bit.ly, shorte.st, link-to.net, up-to-down.net, direct-link.net, file-link.net ``` Above are the supported URL Shorteners. Except these only some URL Shorteners are supported. @@ -236,7 +242,7 @@ OR ``` sudo docker-compose up --build ``` -- To stop Docker : +- To stop Docker: ``` sudo docker ps ``` @@ -350,6 +356,6 @@ Thanks to: - [`breakdowns`](https://github.com/breakdowns) for slam-mirrorbot - [`zevtyardt`](https://github.com/zevtyardt) for some direct links - [`yash-dk`](https://github.com/yash-dk) for implementation of qBittorrent on Python -- [`xyou365`](https://github.com/xyou365) for Service Accounts script. +- [`xyou365`](https://github.com/xyou365) for Service Accounts script And many more people who aren't mentioned here, but can be found in [Contributors](https://github.com/SlamDevs/slam-mirrorbot/graphs/contributors). diff --git a/bot/__main__.py b/bot/__main__.py index 0932883..026560b 100644 --- a/bot/__main__.py +++ b/bot/__main__.py @@ -17,7 +17,7 @@ from bot.helper.telegram_helper.message_utils import * from .helper.ext_utils.bot_utils import get_readable_file_size, get_readable_time from .helper.telegram_helper.filters import CustomFilters from bot.helper.telegram_helper import button_build -from .modules import authorize, list, cancel_mirror, mirror_status, mirror, clone, watch, shell, eval, delete, speedtest, count, reboot +from .modules import authorize, list, cancel_mirror, mirror_status, mirror, clone, watch, shell, eval, torrent_search, delete, speedtest, count, reboot def stats(update, context): @@ -138,6 +138,8 @@ def bot_help(update, context): /{BotCommands.ShellCommand}: Run commands in Shell (Terminal) /{BotCommands.ExecHelpCommand}: Get help for Executor module (Only Owner) + +/{BotCommands.TsHelpCommand}: Get help for Torrent search module ''' help_string = f''' @@ -168,6 +170,8 @@ def bot_help(update, context): /{BotCommands.StatsCommand}: Show Stats of the machine the bot is hosted on /{BotCommands.PingCommand}: Check how long it takes to Ping the Bot + +/{BotCommands.TsHelpCommand}: Get help for Torrent search module ''' if CustomFilters.sudo_user(update) or CustomFilters.owner_filter(update): @@ -194,7 +198,8 @@ botcmds = [ (f'{BotCommands.StatsCommand}','Bot Usage Stats'), (f'{BotCommands.PingCommand}','Ping the Bot'), (f'{BotCommands.RestartCommand}','Restart the bot [owner/sudo only]'), - (f'{BotCommands.LogCommand}','Get the Bot Log [owner/sudo only]') + (f'{BotCommands.LogCommand}','Get the Bot Log [owner/sudo only]'), + (f'{BotCommands.TsHelpCommand}','Get help for Torrent search module') ] diff --git a/bot/helper/custom_filters.py b/bot/helper/custom_filters.py new file mode 100644 index 0000000..0b17f44 --- /dev/null +++ b/bot/helper/custom_filters.py @@ -0,0 +1,23 @@ +from pyrogram import filters + +def callback_data(data): + def func(flt, client, callback_query): + return callback_query.data in flt.data + + data = data if isinstance(data, list) else [data] + return filters.create( + func, + 'CustomCallbackDataFilter', + data=data + ) + +def callback_chat(chats): + def func(flt, client, callback_query): + return callback_query.message.chat.id in flt.chats + + chats = chats if isinstance(chats, list) else [chats] + return filters.create( + func, + 'CustomCallbackChatsFilter', + chats=chats + ) diff --git a/bot/helper/telegram_helper/bot_commands.py b/bot/helper/telegram_helper/bot_commands.py index ad605dd..2283b05 100644 --- a/bot/helper/telegram_helper/bot_commands.py +++ b/bot/helper/telegram_helper/bot_commands.py @@ -27,5 +27,6 @@ class _BotCommands: self.DeleteCommand = 'del' self.ShellCommand = 'shell' self.ExecHelpCommand = 'exechelp' + self.TsHelpCommand = 'tshelp' BotCommands = _BotCommands() diff --git a/bot/modules/torrent_search.py b/bot/modules/torrent_search.py new file mode 100644 index 0000000..e41eb93 --- /dev/null +++ b/bot/modules/torrent_search.py @@ -0,0 +1,339 @@ +import os +import time +import html +import asyncio +import aiohttp +import json +import feedparser +import requests +import itertools + +from telegram.ext import CommandHandler +from telegram import ParseMode + +from urllib.parse import quote as urlencode, urlsplit + +from pyrogram import Client, filters, emoji +from pyrogram.parser import html as pyrogram_html +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton +from pyrogram.handlers import MessageHandler, CallbackQueryHandler + +from bot import app, dispatcher, bot +from bot.helper import custom_filters +from bot.helper.telegram_helper.bot_commands import BotCommands +from bot.helper.telegram_helper.filters import CustomFilters +from bot.helper.telegram_helper.message_utils import sendMessage + +search_lock = asyncio.Lock() +search_info = {False: dict(), True: dict()} + +async def return_search(query, page=1, sukebei=False): + page -= 1 + query = query.lower().strip() + used_search_info = search_info[sukebei] + async with search_lock: + results, get_time = used_search_info.get(query, (None, 0)) + if (time.time() - get_time) > 3600: + results = [] + async with aiohttp.ClientSession() as session: + async with session.get(f'https://{"sukebei." if sukebei else ""}nyaa.si/?page=rss&q={urlencode(query)}') as resp: + d = feedparser.parse(await resp.text()) + text = '' + a = 0 + parser = pyrogram_html.HTML(None) + for i in sorted(d['entries'], key=lambda i: int(i['nyaa_seeders']), reverse=True): + if i['nyaa_size'].startswith('0'): + continue + if not int(i['nyaa_seeders']): + break + link = i['link'] + splitted = urlsplit(link) + if splitted.scheme == 'magnet' and splitted.query: + link = f'{link}' + newtext = f'''{a + 1}. {html.escape(i["title"])} +Link: {link} +Size: {i["nyaa_size"]} +Seeders: {i["nyaa_seeders"]} +Leechers: {i["nyaa_leechers"]} +Category: {i["nyaa_category"]}\n\n''' + futtext = text + newtext + if (a and not a % 10) or len((await parser.parse(futtext))['message']) > 4096: + results.append(text) + futtext = newtext + text = futtext + a += 1 + results.append(text) + ttl = time.time() + used_search_info[query] = results, ttl + try: + return results[page], len(results), ttl + except IndexError: + return '', len(results), ttl + +message_info = dict() +ignore = set() + +@app.on_message(filters.command(['nyaasi', f'nyaasi@{bot.username}'])) +async def nyaa_search(client, message): + text = message.text.split(' ') + text.pop(0) + query = ' '.join(text) + await init_search(client, message, query, False) + +@app.on_message(filters.command(['sukebei', f'sukebei@{bot.username}'])) +async def nyaa_search_sukebei(client, message): + text = message.text.split(' ') + text.pop(0) + query = ' '.join(text) + await init_search(client, message, query, True) + +async def init_search(client, message, query, sukebei): + result, pages, ttl = await return_search(query, sukebei=sukebei) + if not result: + await message.reply_text('No results found') + else: + buttons = [InlineKeyboardButton(f'1/{pages}', 'nyaa_nop'), InlineKeyboardButton(f'Next', 'nyaa_next')] + if pages == 1: + buttons.pop() + reply = await message.reply_text(result, reply_markup=InlineKeyboardMarkup([ + buttons + ])) + message_info[(reply.chat.id, reply.message_id)] = message.from_user.id, ttl, query, 1, pages, sukebei + +@app.on_callback_query(custom_filters.callback_data('nyaa_nop')) +async def nyaa_nop(client, callback_query): + await callback_query.answer(cache_time=3600) + +callback_lock = asyncio.Lock() +@app.on_callback_query(custom_filters.callback_data(['nyaa_back', 'nyaa_next'])) +async def nyaa_callback(client, callback_query): + message = callback_query.message + message_identifier = (message.chat.id, message.message_id) + data = callback_query.data + async with callback_lock: + if message_identifier in ignore: + await callback_query.answer() + return + user_id, ttl, query, current_page, pages, sukebei = message_info.get(message_identifier, (None, 0, None, 0, 0, None)) + og_current_page = current_page + if data == 'nyaa_back': + current_page -= 1 + elif data == 'nyaa_next': + current_page += 1 + if current_page < 1: + current_page = 1 + elif current_page > pages: + current_page = pages + ttl_ended = (time.time() - ttl) > 3600 + if ttl_ended: + text = getattr(message.text, 'html', 'Search expired') + else: + if callback_query.from_user.id != user_id: + await callback_query.answer('...no', cache_time=3600) + return + text, pages, ttl = await return_search(query, current_page, sukebei) + buttons = [InlineKeyboardButton(f'Prev', 'nyaa_back'), InlineKeyboardButton(f'{current_page}/{pages}', 'nyaa_nop'), InlineKeyboardButton(f'Next', 'nyaa_next')] + if ttl_ended: + buttons = [InlineKeyboardButton('Search Expired', 'nyaa_nop')] + else: + if current_page == 1: + buttons.pop(0) + if current_page == pages: + buttons.pop() + if ttl_ended or current_page != og_current_page: + await callback_query.edit_message_text(text, reply_markup=InlineKeyboardMarkup([ + buttons + ])) + message_info[message_identifier] = user_id, ttl, query, current_page, pages, sukebei + if ttl_ended: + ignore.add(message_identifier) + await callback_query.answer() + +# Using upstream API based on: https://github.com/Ryuk-me/Torrents-Api +# Implemented by https://github.com/jusidama18 + +class TorrentSearch: + index = 0 + query = None + message = None + response = None + response_range = None + + RESULT_LIMIT = 4 + RESULT_STR = None + + def __init__(self, command: str, source: str, result_str: str): + self.command = command + self.source = source.rstrip('/') + self.RESULT_STR = result_str + + app.add_handler(MessageHandler(self.find, filters.command([command, f'{self.command}@{bot.username}']))) + app.add_handler(CallbackQueryHandler(self.previous, filters.regex(f"{self.command}_previous"))) + app.add_handler(CallbackQueryHandler(self.delete, filters.regex(f"{self.command}_delete"))) + app.add_handler(CallbackQueryHandler(self.next, filters.regex(f"{self.command}_next"))) + + @staticmethod + def format_magnet(string: str): + if not string: + return "" + return string.split('&tr', 1)[0] + + def get_formatted_string(self, values): + string = self.RESULT_STR.format(**values) + extra = "" + if "Files" in values: + tmp_str = "➲[{Quality} - {Type} ({Size})]({Torrent}): `{magnet}`" + extra += "\n".join( + tmp_str.format(**f, magnet=self.format_magnet(f['Magnet'])) + for f in values['Files'] + ) + else: + magnet = values.get('magnet', values.get('Magnet')) # Avoid updating source dict + if magnet: + extra += f"➲Magnet: `{self.format_magnet(magnet)}`" + if (extra): + string += "\n" + extra + return string + + async def update_message(self): + prevBtn = InlineKeyboardButton(f"Prev", callback_data=f"{self.command}_previous") + delBtn = InlineKeyboardButton(f"{emoji.CROSS_MARK}", callback_data=f"{self.command}_delete") + nextBtn = InlineKeyboardButton(f"Next", callback_data=f"{self.command}_next") + + inline = [] + if (self.index != 0): + inline.append(prevBtn) + inline.append(delBtn) + if (self.index != len(self.response_range) - 1): + inline.append(nextBtn) + + res_lim = min(self.RESULT_LIMIT, len(self.response) - self.RESULT_LIMIT*self.index) + result = f"**Page - {self.index+1}**\n\n" + result += "\n\n=======================\n\n".join( + self.get_formatted_string(self.response[self.response_range[self.index]+i]) + for i in range(res_lim) + ) + + await self.message.edit( + result, + reply_markup=InlineKeyboardMarkup([inline]), + parse_mode="markdown", + ) + + async def find(self, client, message): + if len(message.command) < 2: + await message.reply_text(f"Usage: /{self.command} query") + return + + query = urlencode(message.text.split(None, 1)[1]) + self.message = await message.reply_text("Searching") + try: + async with aiohttp.ClientSession() as session: + async with session.get(f"{self.source}/{query}", timeout=15) as resp: + if (resp.status != 200): + raise Exception('unsuccessful request') + result = await resp.json() + if (result and isinstance(result[0], list)): + result = list(itertools.chain(*result)) + self.response = result + self.response_range = range(0, len(self.response), self.RESULT_LIMIT) + except: + await self.message.edit("No Results Found.") + return + await self.update_message() + + async def delete(self, client, message): + index = 0 + query = None + message = None + response = None + response_range = None + await self.message.delete() + + async def previous(self, client, message): + self.index -= 1 + await self.update_message() + + async def next(self, client, message): + self.index += 1 + await self.update_message() + +RESULT_STR_1337 = ( + "➲Name: `{Name}`\n" + "➲Size: {Size}\n" + "➲Seeders: {Seeders} || ➲Leechers: {Leechers}" +) +RESULT_STR_PIRATEBAY = ( + "➲Name: `{Name}`\n" + "➲Size: {Size}\n" + "➲Seeders: {Seeders} || ➲Leechers: {Leechers}" +) +RESULT_STR_TGX = ( + "➲Name: `{Name}`\n" + "➲Size: {Size}\n" + "➲Seeders: {Seeders} || ➲Leechers: {Leechers}" +) +RESULT_STR_YTS = ( + "➲Name: `{Name}`\n" + "➲Released on: {ReleasedDate}\n" + "➲Genre: {Genre}\n" + "➲Rating: {Rating}\n" + "➲Likes: {Likes}\n" + "➲Duration: {Runtime}\n" + "➲Language: {Language}" +) +RESULT_STR_EZTV = ( + "➲Name: `{Name}`\n" + "➲Size: {Size}\n" + "➲Seeders: {Seeders}" +) +RESULT_STR_TORLOCK = ( + "➲Name: `{Name}`\n" + "➲Size: {Size}\n" + "➲Seeders: {Seeders} || ➲Leechers: {Leechers}" +) +RESULT_STR_RARBG = ( + "➲Name: `{Name}`\n" + "➲Size: {Size}\n" + "➲Seeders: {Seeders} || ➲Leechers: {Leechers}" +) +RESULT_STR_ALL = ( + "➲Name: `{Name}`\n" + "➲Size: {Size}\n" + "➲Seeders: {Seeders} || ➲Leechers: {Leechers}" +) + +torrents_dict = { + '1337x': {'source': "https://torrents-api.netlify.app/.netlify/functions/api/1337x/", 'result_str': RESULT_STR_1337}, + 'piratebay': {'source': "https://torrents-api.netlify.app/.netlify/functions/api/piratebay/", 'result_str': RESULT_STR_PIRATEBAY}, + 'tgx': {'source': "https://torrents-api.netlify.app/.netlify/functions/api/tgx/", 'result_str': RESULT_STR_TGX}, + 'yts': {'source': "https://torrents-api.netlify.app/.netlify/functions/api/yts/", 'result_str': RESULT_STR_YTS}, + 'eztv': {'source': "https://torrents-api.netlify.app/.netlify/functions/api/eztv/", 'result_str': RESULT_STR_EZTV}, + 'torlock': {'source': "https://torrents-api.netlify.app/.netlify/functions/api/torlock/", 'result_str': RESULT_STR_TORLOCK}, + 'rarbg': {'source': "https://torrents-api.netlify.app/.netlify/functions/api/rarbg/", 'result_str': RESULT_STR_RARBG}, + 'ts': {'source': "https://torrents-api.netlify.app/.netlify/functions/api/all/", 'result_str': RESULT_STR_ALL} +} + +torrent_handlers = [] +for command, value in torrents_dict.items(): + torrent_handlers.append(TorrentSearch(command, value['source'], value['result_str'])) + +def searchhelp(update, context): + help_string = ''' +Torrent Search +• /nyaasi [search query] +• /sukebei [search query] +• /1337x [search query] +• /piratebay [search query] +• /tgx [search query] +• /yts [search query] +• /eztv [search query] +• /torlock [search query] +• /rarbg [search query] +• /ts [search query] +''' + sendMessage(help_string, context.bot, update) + + +SEARCHHELP_HANDLER = CommandHandler(BotCommands.TsHelpCommand, searchhelp, filters=(CustomFilters.authorized_chat | CustomFilters.authorized_user) & CustomFilters.mirror_owner_filter, run_async=True) +dispatcher.add_handler(SEARCHHELP_HANDLER)