From 30f9e60c6fde9fcee0efc87fbae067fbcb4d0abc Mon Sep 17 00:00:00 2001 From: ghost Date: Tue, 14 Nov 2023 00:24:19 +0200 Subject: [PATCH] add yggdrasil mirror port for rutracker --- ...racker_yggdrasil_321_c99a_91a1_cd2c__18.py | 319 ++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 engines/rutracker_yggdrasil_321_c99a_91a1_cd2c__18.py diff --git a/engines/rutracker_yggdrasil_321_c99a_91a1_cd2c__18.py b/engines/rutracker_yggdrasil_321_c99a_91a1_cd2c__18.py new file mode 100644 index 0000000..036346d --- /dev/null +++ b/engines/rutracker_yggdrasil_321_c99a_91a1_cd2c__18.py @@ -0,0 +1,319 @@ +# VERSION: 1.9 +# AUTHORS: imDMG [imdmgg@gmail.com] + +# rutracker.org search engine plugin for qBittorrent + +# Yggdrasil mirror by +# http://[321:c99a:91a1:cd2c::18]/ + +# Adaptation by YGGverse (for Yggdrasil issues report) +# https://github.com/YGGverse/qBt_SE/blob/yggdrasil/ + +import base64 +import json +import logging +import re +import sys +import time +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass, field +from html import unescape +from http.cookiejar import MozillaCookieJar +from pathlib import Path +from tempfile import NamedTemporaryFile +from typing import Callable +from urllib.error import URLError, HTTPError +from urllib.parse import urlencode, unquote, quote +from urllib.request import build_opener, HTTPCookieProcessor, ProxyHandler + +try: + from novaprinter import prettyPrinter +except ImportError: + sys.path.insert(0, str(Path(__file__).parent.parent.absolute())) + from novaprinter import prettyPrinter + +FILE = Path(__file__) +BASEDIR = FILE.parent.absolute() + +FILENAME = FILE.stem +FILE_J, FILE_C = [BASEDIR / (FILENAME + fl) for fl in (".json", ".cookie")] + +RE_TORRENTS = re.compile( + r'(.+?).+?data-ts_text="([-\d]+?)">.+?Личи">(\d+?)', + re.S +) +RE_RESULTS = re.compile(r"Результатов\sпоиска:\s(\d{1,3})\s range: + return range(PAGES, -(-t // PAGES) * PAGES, PAGES) + + +class EngineError(Exception): + ... + + +@dataclass +class Config: + username: str = "USERNAME" + password: str = "PASSWORD" + torrent_date: bool = True + proxy: bool = False + # dynamic_proxy: bool = True + proxies: dict = field(default_factory=lambda: {"http": "", "https": ""}) + ua: str = ("Mozilla/5.0 (X11; Linux i686; rv:38.0) Gecko/20100101 " + "Firefox/38.0 ") + + def __post_init__(self): + try: + if not self._validate_json(json.loads(FILE_J.read_text())): + raise ValueError("Incorrect json scheme.") + except Exception as e: + logger.error(e) + FILE_J.write_text(self.to_str()) + (BASEDIR / f"{FILENAME}.ico").write_bytes(base64.b64decode(ICON)) + + def to_str(self) -> str: + return json.dumps(self.to_dict(), indent=4) + + def to_dict(self) -> dict: + return {self._to_camel(k): v for k, v in self.__dict__.items()} + + def _validate_json(self, obj: dict) -> bool: + is_valid = True + for k, v in self.__dict__.items(): + _val = obj.get(self._to_camel(k)) + if type(_val) is not type(v): + is_valid = False + continue + if type(_val) is dict: + for dk, dv in v.items(): + if type(_val.get(dk)) is not type(dv): + _val[dk] = dv + is_valid = False + setattr(self, k, _val) + return is_valid + + @staticmethod + def _to_camel(s: str) -> str: + return "".join(x.title() if i else x + for i, x in enumerate(s.split("_"))) + + +config = Config() + + +class Rutracker_yggdrasil_321_c99a_91a1_cd2c__18: + name = "Rutracker Yggdrasil [321:c99a:91a1:cd2c::18]" + url = "http://[321:c99a:91a1:cd2c::18]/forum/" + url_dl = url + "dl.php?t=" + url_login = url + "login.php" + supported_categories = {"all": "-1"} + + # cookies + mcj = MozillaCookieJar() + # establish connection + session = build_opener(HTTPCookieProcessor(mcj)) + + def search(self, what: str, cat: str = "all") -> None: + self._catch_errors(self._search, what, cat) + + def download_torrent(self, url: str) -> None: + self._catch_errors(self._download_torrent, url) + + def login(self) -> None: + self.mcj.clear() + + form_data = {"login_username": config.username, + "login_password": config.password, + "login": "Вход"} + logger.debug(f"Login. Data before: {form_data}") + # encoding to cp1251 then do default encode whole string + data_encoded = urlencode(form_data, encoding="cp1251").encode() + logger.debug(f"Login. Data after: {data_encoded}") + self._request(self.url_login, data_encoded) + logger.debug(f"That we have: {[cookie for cookie in self.mcj]}") + if "bb_session" not in [cookie.name for cookie in self.mcj]: + raise EngineError( + "We not authorized, please check your credentials!" + ) + self.mcj.save(FILE_C, ignore_discard=True, ignore_expires=True) + logger.info("We successfully authorized") + + def searching(self, query: str, first: bool = False) -> int: + page, torrents_found = self._request(query).decode("cp1251"), -1 + if first: + # check login status + if "log-out-icon" not in page: + if "login-form-full" not in page: + raise EngineError("Unexpected page content") + logger.debug("Looks like we lost session id, lets login") + self.login() + # retry request because guests cant search + page = self._request(query).decode("cp1251") + # firstly we check if there is a result + try: + torrents_found = int(RE_RESULTS.search(page)[1]) + except TypeError: + raise EngineError("Unexpected page content") + if torrents_found <= 0: + return 0 + self.draw(page) + + return torrents_found + + def draw(self, html: str) -> None: + for tor in RE_TORRENTS.findall(html): + local = time.strftime("%y.%m.%d", time.localtime(int(tor[5]))) + torrent_date = f"[{local}] " if config.torrent_date else "" + + prettyPrinter({ + "engine_url": self.url, + "desc_link": self.url + "viewtopic.php?t=" + tor[0], + "name": torrent_date + unescape(tor[1]), + "link": self.url_dl + tor[0], + "size": tor[2], + "seeds": max(0, int(tor[3])), + "leech": tor[4] + }) + + def _catch_errors(self, handler: Callable, *args: str): + try: + self._init() + handler(*args) + except EngineError as ex: + self.pretty_error(args[0], str(ex)) + except Exception as ex: + self.pretty_error(args[0], "Unexpected error, please check logs") + logger.exception(ex) + + def _init(self) -> None: + # add proxy handler if needed + if config.proxy: + if not any(config.proxies.values()): + raise EngineError("Proxy enabled, but not set!") + self.session.add_handler(ProxyHandler(config.proxies)) + logger.debug("Proxy is set!") + + # change user-agent + self.session.addheaders = [("User-Agent", config.ua)] + + # load local cookies + try: + self.mcj.load(FILE_C, ignore_discard=True) + if "bb_session" in [cookie.name for cookie in self.mcj]: + # if cookie.expires < int(time.time()) + return logger.info("Local cookies is loaded") + logger.info("Local cookies expired or bad, try to login") + logger.debug(f"That we have: {[cookie for cookie in self.mcj]}") + except FileNotFoundError: + logger.info("Local cookies not exists, try to login") + self.login() + + def _search(self, what: str, cat: str = "all") -> None: + query = PATTERNS[0] % (self.url, quote(unquote(what)), + self.supported_categories[cat]) + + # make first request (maybe it enough) + t0, total = time.time(), self.searching(query, True) + # do async requests + if total > PAGES: + qrs = [PATTERNS[1] % (query, x) for x in rng(total)] + with ThreadPoolExecutor(len(qrs)) as executor: + executor.map(self.searching, qrs, timeout=30) + + logger.debug(f"--- {time.time() - t0} seconds ---") + logger.info(f"Found torrents: {total}") + + def _download_torrent(self, url: str) -> None: + response = self._request(url) + + # Create a torrent file + with NamedTemporaryFile(suffix=".torrent", delete=False) as fd: + fd.write(response) + + # return file path + logger.debug(fd.name + " " + url) + print(fd.name + " " + url) + + def _request( + self, url: str, data: bytes = None, repeated: bool = False + ) -> bytes: + try: + with self.session.open(url, data, 5) as r: + # checking that tracker isn't blocked + if r.geturl().startswith((self.url, self.url_dl)): + return r.read() + raise EngineError(f"{url} is blocked. Try another proxy.") + except (URLError, HTTPError) as err: + error = str(err.reason) + reason = f"{url} is not response! Maybe it is blocked." + if "timed out" in error and not repeated: + logger.debug("Request timed out. Repeating...") + return self._request(url, data, True) + if "no host given" in error: + reason = "Proxy is bad, try another!" + elif hasattr(err, "code"): + reason = f"Request to {url} failed with status: {err.code}" + + raise EngineError(reason) + + def pretty_error(self, what: str, error: str) -> None: + prettyPrinter({ + "engine_url": self.url, + "desc_link": "https://github.com/YGGverse/qBt_SE/tree/yggdrasil/", + "name": f"[{unquote(what)}][Error]: {error}", + "link": self.url + "error", + "size": "1 TB", # lol + "seeds": 100, + "leech": 100 + }) + + +# pep8 +rutracker_yggdrasil_321_c99a_91a1_cd2c__18 = Rutracker_yggdrasil_321_c99a_91a1_cd2c__18 + +if __name__ == "__main__": + if BASEDIR.parent.joinpath("settings_gui.py").exists(): + from settings_gui import EngineSettingsGUI + + EngineSettingsGUI(FILENAME) + engine = rutracker_yggdrasil_321_c99a_91a1_cd2c__18() + engine.search("doctor")