# █ █ █ █▄▀ ▄▀█ █▀▄▀█ █▀█ █▀█ █ █ # █▀█ █ █ █ █▀█ █ ▀ █ █▄█ █▀▄ █▄█ # 🔒 Licensed under the GNU GPLv3 # 🌐 https://www.gnu.org/licenses/agpl-3.0.html # 👤 https://t.me/hikamoru # requires: bs4 cloudscraper loguru tqdm lxml # meta developer: @hikamorumods import os import pathlib import shutil import string import random import logging from tqdm import tqdm from dataclasses import dataclass from bs4 import BeautifulSoup from cloudscraper import create_scraper, CloudScraper from telethon.tl.types import DocumentAttributeVideo from aiogram.types import CallbackQuery from .. import loader, utils logger = logging.getLogger(__name__) HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.0.0 Safari/537.36", } @dataclass class Season: title: str episodes_urls: list[str] def download_video(url: str, path, scraper: CloudScraper): with scraper.get(url, stream=True) as r: total_length = int(r.headers.get("Content-Length")) with tqdm.wrapattr(r.raw, "read", total=total_length, desc="") as raw: with open(path, "wb") as file: shutil.copyfileobj(raw, file) def remove_symbols(filename: str) -> str: if not filename: return filename forbidden = '\\/*:?|"<>' for symbol in forbidden: filename.replace(symbol, "") return filename class JutSuD: def loader(self, anime_url, season_from, episode_from, season_to, episode_to): scraper = create_scraper( delay=1, browser={ "custom": "ScraperBot/1.0", }, ) response = scraper.get(anime_url) soup = BeautifulSoup(response.text, "lxml") anime_title = soup.find("h1", {"class": "anime_padding_for_title"}).text anime_title = ( anime_title.replace("Смотреть", "") .replace("все серии", "") .replace("и сезоны", "") .strip() ) seasons = [ Season( title=season_title.text, episodes_urls=[], ) for season_title in soup.find_all("h2", class_=["the-anime-season"]) ] if not seasons: seasons.append( Season( title=anime_title, episodes_urls=[], ) ) episodes_soup = soup.find_all( "a", class_=[ "short-btn black video the_hildi", "short-btn green video the_hildi", ], ) current_season_index = -1 current_episode_class = None for ep in episodes_soup: if ep["class"] != current_episode_class: current_episode_class = ep["class"] current_season_index += 1 url = "https://jut.su" + ep["href"] seasons[current_season_index].episodes_urls.append(url) for i, season in enumerate(seasons): season_number = i + 1 if season_number < season_from or season_number > season_to: continue for j, episode_url in enumerate(season.episodes_urls): episode_number = j + 1 if (season_number == season_from and episode_number < episode_from) or ( (season_number == season_to or season_number == len(seasons)) and episode_number > episode_to ): continue response = scraper.get(episode_url) soup = BeautifulSoup(response.content, "lxml") try: episode_title = ( soup.find("div", {"class": "video_plate_title"}).find("h2").text ) except AttributeError: episode_title = soup.find("span", {"itemprop": "name"}).text episode_title = ( episode_title.replace("Смотреть", "") .replace(anime_title, "") .strip() ) video_url = soup.find("source")["src"] name_video = random.choices("".join(string.ascii_letters), k=10) video_path = pathlib.Path(f"{''.join(name_video)}.mp4") episode_slug = f"{season.title} - {episode_title} [#{episode_number}]" try: download_video(url=video_url, path=video_path, scraper=scraper) return video_path, episode_slug except Exception as e: logger.exception(e) return False, False def get_info(self, url): scraper = create_scraper() response = scraper.get(url, headers=HEADERS) soup = BeautifulSoup(response.text, "lxml") anime_title = soup.find("h1", {"class": "anime_padding_for_title"}).text anime_title = ( anime_title.replace("Смотреть", "") .replace("все серии", "") .replace("и сезоны", "") .strip() ) seasons = [ Season( title=season_title, episodes_urls=[], ) for season_title in soup.find_all("h2", class_=["the-anime-season"]) ] if not seasons: seasons.append( Season( title=anime_title, episodes_urls=[], ) ) episodes_soup = soup.find_all( "a", class_=[ "short-btn black video the_hildi", "short-btn green video the_hildi", ], ) return anime_title, seasons, episodes_soup @loader.tds class Jutsu(loader.Module): """Download and get info about anime from jut.su""" strings = { "name": "Jutsu", "info": ( "📺 Anime info\n\n" "Title: {}\n" "Seasons: {}\n" "Total episodes: {}\n" "Link: {}" ), "download_button": "📥 Download", "done": "✅ Download completed!", "choose_season": "📺 Choose season", "choose_episode": "🪶 Choose episode", "wrong_url": "❌ Wrong url!", "no_args": "❌ No args!", "download": "📥 Downloading episode {}... (speed depends on your internet connection)", "close": "❌ Close", } strings_ru = { "info": ( "📺 Информация о аниме\n\n" "Название: {}\n" "Сезонов: {}\n" "Всего серий: {}\n" "Ссылка: {}" ), "download_button": "📥 Скачать", "done": "✅ Скачивание завершено!", "choose_season": "📺 Выберите сезон", "choose_episode": "🪶 Выберите серию", "wrong_url": "❌ Неверная ссылка!", "no_args": "❌ Нет аргументов!", "download": "📥 Скачиваем серию {}... (скорость скачивание зависит от вашего интернета)", "close": "❌ Закрыть", } async def client_ready(self, client, db): asset_ch, _ = await utils.asset_channel( self._client, "JutSu downloads", "Downloaded anime from JutSu will be sent here. (Hikamoru back?)", avatar="https://i.pinimg.com/564x/0a/da/0b/0ada0bb575146736679f5ea7a78971b8.jpg", ) self.chid = int(f"-100{asset_ch.id}") async def download_(self, call, url, seasons, episodes_soup): seasons = [season for season in range(1, len(seasons) + 1)] kb = [] for mod_row in utils.chunks(seasons, 3): row = [ { "text": f"• {season} •", "callback": self.season_, "args": (season, episodes_soup, url), } for season in mod_row ] kb += [row] await call.edit(self.strings["choose_season"], reply_markup=kb) async def season_(self, call, season, eps, url): episodes = [episode for episode in range(1, len(eps) + 1)] kb = [] for mod_row in utils.chunks(episodes, 3): row = [ { "text": f"• {episode} •", "callback": self.episod_, "args": (episode, season, url), } for episode in mod_row ] kb += [row] await call.edit(self.strings["choose_episode"], reply_markup=kb) async def episod_(self, call: CallbackQuery, episode, episode_number, url): await call.edit(self.strings["download"].format(episode_number)) try: name, title = JutSuD().loader( url, episode_number, episode, episode_number, episode ) except TypeError: await call.edit("There is not such a episode (This bug with button will be fixed soon)") await self.client.send_file( self.chid, open(name, "rb"), caption=self.strings["done"] + f"\n\n{title}", filetype="video", attributes=(DocumentAttributeVideo(0, 0, 0),), ) await call.edit(self.strings["done"]) os.remove(name) async def close_(self, call): await call.delete() @loader.command() async def jutsud(self, message): """Download anime from jutsu - [url]""" args = utils.get_args_raw(message) if not args: await utils.answer(message, self.strings["no_args"]) return if not args.startswith("https://jut.su"): await utils.answer(message, self.strings["wrong_url"]) return anime_title, seasons, episodes_soup = JutSuD().get_info(args) await utils.answer( message, self.strings["info"].format( anime_title, len(seasons), len(episodes_soup), args ), reply_markup=[ [ { "text": self.strings["download_button"], "callback": self.download_, "kwargs": { "url": args, "seasons": seasons, "episodes_soup": episodes_soup, }, }, { "text": self.strings["close"], "callback": self.close_, }, ] ], )