From e72f6eee0c30d1d7f25c03910c030cdd06a21fd2 Mon Sep 17 00:00:00 2001 From: imDMG Date: Sun, 10 Sep 2023 18:14:38 +0500 Subject: [PATCH] better handling errors --- engines/nnmclub.py | 256 +++++++++++++++++++++------------------------ 1 file changed, 120 insertions(+), 136 deletions(-) diff --git a/engines/nnmclub.py b/engines/nnmclub.py index 46e6664..fc503ec 100644 --- a/engines/nnmclub.py +++ b/engines/nnmclub.py @@ -1,4 +1,4 @@ -# VERSION: 2.9 +# VERSION: 2.10 # AUTHORS: imDMG [imdmgg@gmail.com] # NoNaMe-Club search engine plugin for qBittorrent @@ -12,10 +12,10 @@ import time from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass, field from html import unescape -from http.cookiejar import Cookie, MozillaCookieJar +from http.cookiejar import MozillaCookieJar from pathlib import Path from tempfile import NamedTemporaryFile -from typing import Optional, Union +from typing import Callable from urllib.error import URLError, HTTPError from urllib.parse import urlencode, unquote from urllib.request import build_opener, HTTPCookieProcessor, ProxyHandler @@ -29,24 +29,20 @@ except ImportError: FILE = Path(__file__) BASEDIR = FILE.parent.absolute() -FILENAME = FILE.name[:-3] -FILE_J, FILE_C = [BASEDIR / (FILENAME + fl) for fl in [".json", ".cookie"]] - -PAGES = 50 - - -def rng(t: int) -> range: - return range(PAGES, -(-t // PAGES) * PAGES, PAGES) +FILENAME = FILE.stem +FILE_J, FILE_C = [BASEDIR / (FILENAME + fl) for fl in (".json", ".cookie")] RE_TORRENTS = re.compile( r'topictitle"\shref="(.+?)">(.+?).+?href="(d.+?)".+?(\d+?).+?' - r'(\d+).+?(\d+).+?(\d+)', re.S + r'(\d+?).+?(\d+?).+?(\d+?)', re.S ) RE_RESULTS = re.compile(r'TP_VER">(?:Результатов\sпоиска:\s(\d{1,3}))?\s', re.S) RE_CODE = re.compile(r'name="code"\svalue="(.+?)"', re.S) PATTERNS = ("%stracker.php?nm=%s&%s", "%s&start=%s") +PAGES = 50 + # base64 encoded image ICON = ("AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAAAAAAAAAAA" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaQicAXRQFADICAQAHAAAA" @@ -81,6 +77,14 @@ logging.basicConfig( logger = logging.getLogger(__name__) +def rng(t: int) -> range: + return range(PAGES, -(-t // PAGES) * PAGES, PAGES) + + +class EngineError(Exception): + ... + + @dataclass class Config: username: str = "USERNAME" @@ -103,7 +107,7 @@ class Config: (BASEDIR / f"{FILENAME}.ico").write_bytes(base64.b64decode(ICON)) def to_str(self) -> str: - return json.dumps(self.to_dict(), indent=4, sort_keys=False) + 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()} @@ -144,93 +148,22 @@ class NNMClub: "games": "17", "anime": "24", "software": "21"} - - # error message - error: Optional[str] = None # cookies mcj = MozillaCookieJar() # establish connection session = build_opener(HTTPCookieProcessor(mcj)) - def __init__(self): - # add proxy handler if needed - if config.proxy: - if any(config.proxies.values()): - self.session.add_handler(ProxyHandler(config.proxies)) - logger.debug("Proxy is set!") - else: - self.error = "Proxy enabled, but not set!" - - # change user-agent - self.session.addheaders = [("User-Agent", config.ua)] - - # load local cookies - try: - self.mcj.load(FILE_C, ignore_discard=True) - if "phpbb2mysql_4_data" in [cookie.name for cookie in self.mcj]: - # if cookie.expires < int(time.time()) - logger.info("Local cookies is loaded") - else: - logger.info("Local cookies expired or bad") - logger.debug(f"That we have: {[cookie for cookie in self.mcj]}") - self.mcj.clear() - self.login() - except FileNotFoundError: - self.login() - def search(self, what: str, cat: str = "all") -> None: - if self.error: - self.pretty_error(what) - return None - c = self.supported_categories[cat] - query = PATTERNS[0] % (self.url, what.replace(" ", "+"), - "f=-1" if c == "-1" else "c=" + c) - - # make first request (maybe it enough) - t0, total = time.time(), self.searching(query, True) - if self.error: - self.pretty_error(what) - return None - # 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}") + self._catch_errors(self._search, what, cat) def download_torrent(self, url: str) -> None: - # Download url - response = self._request(url) - if self.error: - self.pretty_error(url) - return None - - # 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) + self._catch_errors(self._download_torrent, url) def login(self) -> None: - if self.error: - return None - - # if we wanna use https we mast add ssl=enable_ssl to cookie - self.mcj.set_cookie(Cookie(0, "ssl", "enable_ssl", None, False, - ".nnmclub.to", True, False, "/", True, - False, None, False, None, None, {})) - - response = self._request(self.url_login) - if self.error: - return None - result = RE_CODE.search(response.decode("cp1251")) - if not result: - self.error = "Unexpected page content" - return None + self.mcj.clear() + + logger.debug("Make initial login request") + result = RE_CODE.search(self._request(self.url_login).decode("cp1251")) form_data = {"username": config.username, "password": config.password, @@ -239,38 +172,29 @@ class NNMClub: "login": "Вход"} # encoding to cp1251 then do default encode whole string data_encoded = urlencode(form_data, encoding="cp1251").encode() - self._request(self.url_login, data_encoded) - if self.error: - return None + logger.debug(f"That we have: {[cookie for cookie in self.mcj]}") - if "phpbb2mysql_4_sid" in [cookie.name for cookie in self.mcj]: - self.mcj.save(FILE_C, ignore_discard=True, ignore_expires=True) - logger.info("We successfully authorized") - else: - self.error = "We not authorized, please check your credentials!" - logger.warning(self.error) - - def searching(self, query: str, first: bool = False) -> Union[None, int]: - response = self._request(query) - if self.error: - return None - page, torrents_found = response.decode("cp1251"), -1 + if "phpbb2mysql_4_sid" 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 f"Выход [ {config.username} ]" not in page: logger.debug("Looks like we lost session id, lets login") - self.mcj.clear() self.login() - if self.error: - return None # firstly we check if there is a result - result = RE_RESULTS.search(page) - if not result: - self.error = "Unexpected page content" - return None - torrents_found = int(result[1]) - if not torrents_found: + 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) @@ -293,40 +217,100 @@ class NNMClub: "leech": tor[5] }) + 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 "phpbb2mysql_4_data" 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: + c = self.supported_categories[cat] + query = PATTERNS[0] % (self.url, what.replace(" ", "+"), + "f=-1" if c == "-1" else "c=" + c) + + # 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: + # Download url + 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: Optional[bytes] = None, repeated: bool = False - ) -> Union[bytes, None]: + 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() - self.error = f"{url} is blocked. Try another proxy." + raise EngineError(f"{url} is blocked. Try another proxy.") except (URLError, HTTPError) as err: - logger.error(err.reason) error = str(err.reason) + reason = f"{url} is not response! Maybe it is blocked." if "timed out" in error and not repeated: - logger.debug("Repeating request...") + logger.debug("Request timed out. Repeating...") return self._request(url, data, True) if "no host given" in error: - self.error = "Proxy is bad, try another!" + reason = "Proxy is bad, try another!" elif hasattr(err, "code"): - self.error = f"Request to {url} failed with status: {err.code}" - else: - self.error = f"{url} is not response! Maybe it is blocked." - - return None - - def pretty_error(self, what: str) -> None: - prettyPrinter({"engine_url": self.url, - "desc_link": "https://github.com/imDMG/qBt_SE", - "name": f"[{unquote(what)}][Error]: {self.error}", - "link": self.url + "error", - "size": "1 TB", # lol - "seeds": 100, - "leech": 100}) - - self.error = None + 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/imDMG/qBt_SE", + "name": f"[{unquote(what)}][Error]: {error}", + "link": self.url + "error", + "size": "1 TB", # lol + "seeds": 100, + "leech": 100 + }) # pep8