Browse Source

better handling errors

master
imDMG 10 months ago
parent
commit
e72f6eee0c
  1. 256
      engines/nnmclub.py

256
engines/nnmclub.py

@ -1,4 +1,4 @@
# VERSION: 2.9 # VERSION: 2.10
# AUTHORS: imDMG [imdmgg@gmail.com] # AUTHORS: imDMG [imdmgg@gmail.com]
# NoNaMe-Club search engine plugin for qBittorrent # NoNaMe-Club search engine plugin for qBittorrent
@ -12,10 +12,10 @@ import time
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass, field from dataclasses import dataclass, field
from html import unescape from html import unescape
from http.cookiejar import Cookie, MozillaCookieJar from http.cookiejar import MozillaCookieJar
from pathlib import Path from pathlib import Path
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
from typing import Optional, Union from typing import Callable
from urllib.error import URLError, HTTPError from urllib.error import URLError, HTTPError
from urllib.parse import urlencode, unquote from urllib.parse import urlencode, unquote
from urllib.request import build_opener, HTTPCookieProcessor, ProxyHandler from urllib.request import build_opener, HTTPCookieProcessor, ProxyHandler
@ -29,24 +29,20 @@ except ImportError:
FILE = Path(__file__) FILE = Path(__file__)
BASEDIR = FILE.parent.absolute() BASEDIR = FILE.parent.absolute()
FILENAME = FILE.name[:-3] FILENAME = FILE.stem
FILE_J, FILE_C = [BASEDIR / (FILENAME + fl) for fl in [".json", ".cookie"]] 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)
RE_TORRENTS = re.compile( RE_TORRENTS = re.compile(
r'topictitle"\shref="(.+?)"><b>(.+?)</b>.+?href="(d.+?)".+?<u>(\d+?)</u>.+?' r'topictitle"\shref="(.+?)"><b>(.+?)</b>.+?href="(d.+?)".+?<u>(\d+?)</u>.+?'
r'<b>(\d+)</b>.+?<b>(\d+)</b>.+?<u>(\d+)</u>', re.S r'<b>(\d+?)</b>.+?<b>(\d+?)</b>.+?<u>(\d+?)</u>', re.S
) )
RE_RESULTS = re.compile(r'TP_VER">(?:Результатов\sпоиска:\s(\d{1,3}))?\s', 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) RE_CODE = re.compile(r'name="code"\svalue="(.+?)"', re.S)
PATTERNS = ("%stracker.php?nm=%s&%s", "%s&start=%s") PATTERNS = ("%stracker.php?nm=%s&%s", "%s&start=%s")
PAGES = 50
# base64 encoded image # base64 encoded image
ICON = ("AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAAAAAAAAAAA" ICON = ("AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaQicAXRQFADICAQAHAAAA" "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaQicAXRQFADICAQAHAAAA"
@ -81,6 +77,14 @@ logging.basicConfig(
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def rng(t: int) -> range:
return range(PAGES, -(-t // PAGES) * PAGES, PAGES)
class EngineError(Exception):
...
@dataclass @dataclass
class Config: class Config:
username: str = "USERNAME" username: str = "USERNAME"
@ -103,7 +107,7 @@ class Config:
(BASEDIR / f"{FILENAME}.ico").write_bytes(base64.b64decode(ICON)) (BASEDIR / f"{FILENAME}.ico").write_bytes(base64.b64decode(ICON))
def to_str(self) -> str: 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: def to_dict(self) -> dict:
return {self._to_camel(k): v for k, v in self.__dict__.items()} return {self._to_camel(k): v for k, v in self.__dict__.items()}
@ -144,93 +148,22 @@ class NNMClub:
"games": "17", "games": "17",
"anime": "24", "anime": "24",
"software": "21"} "software": "21"}
# error message
error: Optional[str] = None
# cookies # cookies
mcj = MozillaCookieJar() mcj = MozillaCookieJar()
# establish connection # establish connection
session = build_opener(HTTPCookieProcessor(mcj)) 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: def search(self, what: str, cat: str = "all") -> None:
if self.error: self._catch_errors(self._search, what, cat)
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}")
def download_torrent(self, url: str) -> None: def download_torrent(self, url: str) -> None:
# Download url self._catch_errors(self._download_torrent, 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)
def login(self) -> None: def login(self) -> None:
if self.error: self.mcj.clear()
return None
logger.debug("Make initial login request")
# if we wanna use https we mast add ssl=enable_ssl to cookie result = RE_CODE.search(self._request(self.url_login).decode("cp1251"))
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
form_data = {"username": config.username, form_data = {"username": config.username,
"password": config.password, "password": config.password,
@ -239,38 +172,29 @@ class NNMClub:
"login": "Вход"} "login": "Вход"}
# encoding to cp1251 then do default encode whole string # encoding to cp1251 then do default encode whole string
data_encoded = urlencode(form_data, encoding="cp1251").encode() data_encoded = urlencode(form_data, encoding="cp1251").encode()
self._request(self.url_login, data_encoded) self._request(self.url_login, data_encoded)
if self.error:
return None
logger.debug(f"That we have: {[cookie for cookie in self.mcj]}") logger.debug(f"That we have: {[cookie for cookie in self.mcj]}")
if "phpbb2mysql_4_sid" in [cookie.name for cookie in self.mcj]: if "phpbb2mysql_4_sid" not in [cookie.name for cookie in self.mcj]:
self.mcj.save(FILE_C, ignore_discard=True, ignore_expires=True) raise EngineError(
logger.info("We successfully authorized") "We not authorized, please check your credentials!"
else: )
self.error = "We not authorized, please check your credentials!" self.mcj.save(FILE_C, ignore_discard=True, ignore_expires=True)
logger.warning(self.error) logger.info("We successfully authorized")
def searching(self, query: str, first: bool = False) -> Union[None, int]: def searching(self, query: str, first: bool = False) -> int:
response = self._request(query) page, torrents_found = self._request(query).decode("cp1251"), -1
if self.error:
return None
page, torrents_found = response.decode("cp1251"), -1
if first: if first:
# check login status # check login status
if f"Выход [ {config.username} ]" not in page: if f"Выход [ {config.username} ]" not in page:
logger.debug("Looks like we lost session id, lets login") logger.debug("Looks like we lost session id, lets login")
self.mcj.clear()
self.login() self.login()
if self.error:
return None
# firstly we check if there is a result # firstly we check if there is a result
result = RE_RESULTS.search(page) try:
if not result: torrents_found = int(RE_RESULTS.search(page)[1])
self.error = "Unexpected page content" except TypeError:
return None raise EngineError("Unexpected page content")
torrents_found = int(result[1]) if torrents_found <= 0:
if not torrents_found:
return 0 return 0
self.draw(page) self.draw(page)
@ -293,40 +217,100 @@ class NNMClub:
"leech": tor[5] "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( def _request(
self, url: str, data: Optional[bytes] = None, repeated: bool = False self, url: str, data: bytes = None, repeated: bool = False
) -> Union[bytes, None]: ) -> bytes:
try: try:
with self.session.open(url, data, 5) as r: with self.session.open(url, data, 5) as r:
# checking that tracker isn't blocked # checking that tracker isn't blocked
if r.geturl().startswith((self.url, self.url_dl)): if r.geturl().startswith((self.url, self.url_dl)):
return r.read() 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: except (URLError, HTTPError) as err:
logger.error(err.reason)
error = str(err.reason) error = str(err.reason)
reason = f"{url} is not response! Maybe it is blocked."
if "timed out" in error and not repeated: if "timed out" in error and not repeated:
logger.debug("Repeating request...") logger.debug("Request timed out. Repeating...")
return self._request(url, data, True) return self._request(url, data, True)
if "no host given" in error: if "no host given" in error:
self.error = "Proxy is bad, try another!" reason = "Proxy is bad, try another!"
elif hasattr(err, "code"): elif hasattr(err, "code"):
self.error = f"Request to {url} failed with status: {err.code}" reason = f"Request to {url} failed with status: {err.code}"
else:
self.error = f"{url} is not response! Maybe it is blocked." raise EngineError(reason)
return None def pretty_error(self, what: str, error: str) -> None:
prettyPrinter({
def pretty_error(self, what: str) -> None: "engine_url": self.url,
prettyPrinter({"engine_url": self.url, "desc_link": "https://github.com/imDMG/qBt_SE",
"desc_link": "https://github.com/imDMG/qBt_SE", "name": f"[{unquote(what)}][Error]: {error}",
"name": f"[{unquote(what)}][Error]: {self.error}", "link": self.url + "error",
"link": self.url + "error", "size": "1 TB", # lol
"size": "1 TB", # lol "seeds": 100,
"seeds": 100, "leech": 100
"leech": 100}) })
self.error = None
# pep8 # pep8

Loading…
Cancel
Save