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)