From b4e9c16afa4cf3f21f10836f61d5a4de522a36a8 Mon Sep 17 00:00:00 2001 From: imDMG Date: Mon, 16 Dec 2019 22:16:28 +0500 Subject: [PATCH] Many code refactoring (including bringing to a common structure). New error handler. Added rutracker.py plugin. --- .gitignore | 17 ++ gui_kinozal.py | 17 ++ kinozal.ico | Bin 1150 -> 0 bytes kinozal.png | Bin 752 -> 0 bytes kinozal.py | 411 ++++++++++++++++++++++++++++--------------------- nnmclub.ico | Bin 1150 -> 0 bytes nnmclub.png | Bin 912 -> 0 bytes nnmclub.py | 390 +++++++++++++++++++++++++++------------------- rutracker.py | 280 +++++++++++++++++++++++++++++++++ 9 files changed, 780 insertions(+), 335 deletions(-) create mode 100644 .gitignore delete mode 100644 kinozal.ico delete mode 100644 kinozal.png delete mode 100644 nnmclub.ico delete mode 100644 nnmclub.png create mode 100644 rutracker.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ee49a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +/tests/ +/venv/ +/kinozal.cookie +/kinozal.cookie.bak +/kinozal.ico +/kinozal.json +/kinozal.json.bak +/nnmclub.cookie +/nnmclub.cookie.bak +/nnmclub.ico +/nnmclub.json +/nnmclub.json.bak +/rutracker.cookie +/rutracker.cookie.bak +/rutracker.ico +/rutracker.json +/rutracker.json.bak diff --git a/gui_kinozal.py b/gui_kinozal.py index 36cf57a..985faad 100644 --- a/gui_kinozal.py +++ b/gui_kinozal.py @@ -47,6 +47,23 @@ class kinozal(object): "ua": "Mozilla/5.0 (X11; Linux i686; rv:38.0) Gecko/20100101 Firefox/38.0" } + icon = 'AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAQAQAAAAAAAAAAAAAAAAAAAAAAACARztMgEc7/4BHO' \ + '/+ARztMAAAAAIBHO0yhd2n/gEc7/6F3af+ARztMAAAAAIBHO0yARzv/gEc7/4BHO0wAAAAAgEc7/7iYiv/O4+r/pH5x/4FIPP+kfnH' \ + '/zsrE/87j6v/OycL/pYB1/4BHO/+jfHD/ztbV/7+yrP+ARzv/AAAAAIBHO//O4+r/zu/9/87v/f/O7/3/zu/9/87v/f/O7/3/zu/9/87v' \ + '/f/O7/3/zu/9/87v/f/O1dT/gEc7/wAAAACARztMpYB1/87v/f8IC5X/CAuV/wgLlf8IC5X/zu/9/77h+v9vgcv/SFSy/wAAif97j87' \ + '/oXdp/4BHO0wAAAAAAAAAAIBHO//O7/3/gabq/w4Tnv8OE57/gabq/87v/f96muj/DBCd/wAAif83SMf/zu/9/4BHO' \ + '/8AAAAAAAAAAIBHO0ynhXv/zu/9/87v/f8OE57/CAuV/87v/f+63vn/Hyqx/wAAif9KXMX/zO38/87v/f+mhHn/gEc7TAAAAAChd2n' \ + '/1eHk/87v/f/O7/3/DhOe/wgLlf9nhuT/MEPF/wAAif82ScT/utjy/87v/f/O7/3/zsrD/6F3af8AAAAAgEc7/9Pk6v/O7/3/zu/9' \ + '/xQcqP8IC5X/FBqo/xUYlf9of9v/zu/9/87v/f/O7/3/zu/9/87d4f+ARzv/AAAAAIBHO//Y19X/zu/9/87v/f8RGaT/CAuV' \ + '/wAAif90h8v/zu/9/87v/f/O7/3/zu/9/87v/f/OycL/gEc7/wAAAAChd2n/up6S/87v/f/O7/3/ERmk/wgLlf9DXdj/CQ6Z/zdAqf/O7' \ + '/3/zu/9/87v/f/O7/3/upyQ/6F3af8AAAAAgEc7TIJLQP/P7/3/zu/9/xQcqP8IC5X/zu/9/46l2f8jNMD/gJXS/87v/f/O7/3/zu/9' \ + '/45kXf+ARztMAAAAAAAAAACARzv/0e35/5Go2/8UHKj/CAuV/5Go2//O7/3/XHDY/w4Tn/8YHJf/QEms/9Dr9v+ARzv' \ + '/AAAAAAAAAACARztMu6KY/9Hu+v8IC5X/CAuV/wgLlf8IC5X/zu/9/87v/f9OZtz/FB2q/y08wv/Q6/b/oXdp/4BHO0wAAAAAgEc7/9' \ + '/s8P/R7fn/0e77/9Hu+//O7/3/zu/9/87v/f/O7/3/z+/9/9Dt+P/Q7Pf/3u3t/87n8P+ARzv/AAAAAIBHO//Sz8j/3+zw/7qhlf+IWE' \ + '//o31w/9jZ2P/a7fH/2NfV/7ylm/+GVEr/qYyD/87o8f/R2dj/gEc7/wAAAACARztMgEc7/4BHO/+ARztMAAAAAIBHO0yARzv/gEc7' \ + '/4BHO/+ARztMAAAAAIBHO0yARzv/gEc7' \ + '/4BHO0wAAAAACCEAAAABAAAAAQAAAAEAAIADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAACAAwAAAAEAAAABAAAAAQAACCEAAA== ' + def __init__(self): # setup logging into qBittorrent/logs logging.basicConfig(handlers=[logging.FileHandler(self.path_to('../../logs', 'kinozal.log'), 'w', 'utf-8')], diff --git a/kinozal.ico b/kinozal.ico deleted file mode 100644 index 27506145daf4a69785e9b03cc650896a11b2065c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcma)5O-NKx6uwdmqmYTFurMmxL=d!yT;yWFMWYS1anV9|QMPJ=BFcVdtn7!d9~iPf z!*py6H@S*eRG3m}JZH42rfNW(M4X} zJzb-<6Q6Vt*BF_`s`dAoTVC0XSN1Q)TTdM({p=}eNv6_&@%PlFv9Yw9xtq(nU|3&u zjoI^z_u#CLfpkVIt?4a_@%r8sF)S^Np!&pcmM8O6W4=$h{e$&3scRN5Z(%-pb>K{| z#+j%1@=TQnq~B<~qU#jDw93MQf@>)DwdtJdVcfjr)*qQCKjg!Q@k!A7iHV;aYM*MPy~XVxh>w5Ah;76CG^sTZzrXJV z_b{3wIJ004R>004l5008;`004mK004C`008P>0026e000+ooVrmw0003C zP)t-s0000xM}Rv=fXL6%@a*4}p}J34h;V(Q*xA_e?b_AX*qf!iN>qk}jH&VH&e++} z`R&pB?$Q14&i(Jt`0dd5?9lD)-tgznn4-J-?$MPC2;62)svQ)g3a?8Wfcn72AJk;oZ*U;nkiK4&;Vs#X~T}NjCD>y2HxOdxfX@-ny|WAH`fs z{O!zngr@4If#{lgoe&JiNH=wWrTXE%%YkpQR7lQ`d+OuP)YZ<4n7DC%q&!H0#LCWa ze52LY&aAS(|NsBreY42`0007XQchCk0b)x>L|E;$#Et*}00(qQO+^RZ z3JVY;Gh`0^SO5S3)Ja4^R2b7mkLgmvKoEp?hp2#8=yAe0Cy~JM3Qm>ww#`w z3%FolFM-%}y6);4xM7R#3Bc{0>)+#{h(!{|2_6UT2}Nv*;;`qJS2vWyD8s+rBxRK4 z@A*d-rS$prEnSM@mh4~s9}dhJWQ`_i5&!@IC3HntbYx+4WjbSWWnpw>05UK!IV~_T zEipG#F*Q0fHaapeD=;xSFffppnF*GePFfB1L iR53IНайдено\s+?(\d+)\s+?раздач', + r'nam">(.*?).+?s\'>.+?s\'>' + r'(.*?)<.+?sl_s\'>(\d+)<.+?sl_p\'>(\d+)<.+?s\'>(.*?)', + '%sbrowse.php?s=%s&c=%s', "%s&page=%s") + +FILENAME = __file__[__file__.rfind('/') + 1:-3] +FILE_J, FILE_C = [path_to(FILENAME + fe) for fe in ['.json', '.cookie']] + +# base64 encoded image +ICON = ("AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAQAQAAAAAAAAAAA" + "AAAAAAAAAAAACARztMgEc7/4BHO/+ARztMAAAAAIBHO0yhd2n/gEc7/6F3af+ARztMAAAA" + "AIBHO0yARzv/gEc7/4BHO0wAAAAAgEc7/7iYiv/O4+r/pH5x/4FIPP+kfnH/zsrE/87j6v" + "/OycL/pYB1/4BHO/+jfHD/ztbV/7+yrP+ARzv/AAAAAIBHO//O4+r/zu/9/87v/f/O7/3/" + "zu/9/87v/f/O7/3/zu/9/87v/f/O7/3/zu/9/87v/f/O1dT/gEc7/wAAAACARztMpYB1/8" + "7v/f8IC5X/CAuV/wgLlf8IC5X/zu/9/77h+v9vgcv/SFSy/wAAif97j87/oXdp/4BHO0wA" + "AAAAAAAAAIBHO//O7/3/gabq/w4Tnv8OE57/gabq/87v/f96muj/DBCd/wAAif83SMf/zu" + "/9/4BHO/8AAAAAAAAAAIBHO0ynhXv/zu/9/87v/f8OE57/CAuV/87v/f+63vn/Hyqx/wAA" + "if9KXMX/zO38/87v/f+mhHn/gEc7TAAAAAChd2n/1eHk/87v/f/O7/3/DhOe/wgLlf9nhu" + "T/MEPF/wAAif82ScT/utjy/87v/f/O7/3/zsrD/6F3af8AAAAAgEc7/9Pk6v/O7/3/zu/9" + "/xQcqP8IC5X/FBqo/xUYlf9of9v/zu/9/87v/f/O7/3/zu/9/87d4f+ARzv/AAAAAIBHO/" + "/Y19X/zu/9/87v/f8RGaT/CAuV/wAAif90h8v/zu/9/87v/f/O7/3/zu/9/87v/f/OycL/" + "gEc7/wAAAAChd2n/up6S/87v/f/O7/3/ERmk/wgLlf9DXdj/CQ6Z/zdAqf/O7/3/zu/9/8" + "7v/f/O7/3/upyQ/6F3af8AAAAAgEc7TIJLQP/P7/3/zu/9/xQcqP8IC5X/zu/9/46l2f8j" + "NMD/gJXS/87v/f/O7/3/zu/9/45kXf+ARztMAAAAAAAAAACARzv/0e35/5Go2/8UHKj/CA" + "uV/5Go2//O7/3/XHDY/w4Tn/8YHJf/QEms/9Dr9v+ARzv/AAAAAAAAAACARztMu6KY/9Hu" + "+v8IC5X/CAuV/wgLlf8IC5X/zu/9/87v/f9OZtz/FB2q/y08wv/Q6/b/oXdp/4BHO0wAAA" + "AAgEc7/9/s8P/R7fn/0e77/9Hu+//O7/3/zu/9/87v/f/O7/3/z+/9/9Dt+P/Q7Pf/3u3t" + "/87n8P+ARzv/AAAAAIBHO//Sz8j/3+zw/7qhlf+IWE//o31w/9jZ2P/a7fH/2NfV/7ylm/" + "+GVEr/qYyD/87o8f/R2dj/gEc7/wAAAACARztMgEc7/4BHO/+ARztMAAAAAIBHO0yARzv/" + "gEc7/4BHO/+ARztMAAAAAIBHO0yARzv/gEc7/4BHO0wAAAAACCEAAAABAAAAAQAAAAEAAI" + "ADAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAACAAwAAAAEAAAABAAAAAQAACCEAAA== ") + +# setup logging +logging.basicConfig( + format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s", + datefmt="%m-%d %H:%M") +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +try: + # try to load user data from file + with open(FILE_J, 'r+') as f: + cfg = json.load(f) + if "version" not in cfg.keys(): + cfg.update({"version": 2, "torrentDate": True}) + f.seek(0) + f.write(json.dumps(cfg, indent=4, sort_keys=False)) + f.truncate() + config = cfg + logger.debug("Config is loaded.") +except OSError as e: + logger.error(e) + # if file doesn't exist, we'll create it + with open(FILE_J, 'w') as f: + f.write(json.dumps(config, indent=4, sort_keys=False)) + # also write/rewrite ico file + with open(path_to(FILENAME + '.ico'), 'wb') as f: + f.write(base64.b64decode(ICON)) + logger.debug("Write files.") + class kinozal(object): name = 'Kinozal' - url = 'http://kinozal.tv' + url = 'http://kinozal.tv/' supported_categories = {'all': '0', 'movies': '1002', 'tv': '1001', @@ -29,214 +118,188 @@ class kinozal(object): 'anime': '20', 'software': '32'} - # default config for kinozal.json - config = { - "version": 2, - "torrentDate": True, - "username": "USERNAME", - "password": "PASSWORD", - "proxy": False, - "proxies": { - "http": "", - "https": "" - }, - "magnet": True, - "ua": "Mozilla/5.0 (X11; Linux i686; rv:38.0) Gecko/20100101 Firefox/38.0" - } - def __init__(self): - # setup logging into qBittorrent/logs - logging.basicConfig(handlers=[logging.FileHandler(self.path_to('../../logs', 'kinozal.log'), 'w', 'utf-8')], - level=logging.DEBUG, - format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', - datefmt='%m-%d %H:%M') - - try: - # try to load user data from file - with open(self.path_to('kinozal.json'), 'r+') as f: - config = json.load(f) - if "version" not in config.keys(): - config.update({"version": 2, "torrentDate": True}) - f.seek(0) - f.write(json.dumps(config, indent=4, sort_keys=False)) - f.truncate() - self.config = config - except OSError as e: - logging.error(e) - # if file doesn't exist, we'll create it - with open(self.path_to('kinozal.json'), 'w') as f: - f.write(json.dumps(self.config, indent=4, sort_keys=False)) + # error message + self.error = None # establish connection self.session = build_opener() # add proxy handler if needed - if self.config['proxy'] and any(self.config['proxies'].keys()): - self.session.add_handler(ProxyHandler(self.config['proxies'])) + 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.pop() - self.session.addheaders.append(('User-Agent', self.config['ua'])) - - # avoid endless waiting - self.blocked = False + self.session.addheaders.append(('User-Agent', config['ua'])) - mcj = MozillaCookieJar() - cookie_file = os.path.abspath(os.path.join(os.path.dirname(__file__), 'kinozal.cookie')) # load local cookies - if os.path.isfile(cookie_file): - mcj.load(cookie_file, ignore_discard=True) + mcj = MozillaCookieJar() + try: + mcj.load(FILE_C, ignore_discard=True) if 'uid' in [cookie.name for cookie in mcj]: # if cookie.expires < int(time.time()) - logging.info("Local cookies is loaded") + logger.info("Local cookies is loaded") self.session.add_handler(HTTPCookieProcessor(mcj)) else: - logging.info("Local cookies expired or bad") - logging.debug(f"That we have: {[cookie for cookie in mcj]}") + logger.info("Local cookies expired or bad") + logger.debug(f"That we have: {[cookie for cookie in mcj]}") mcj.clear() - self.login(mcj, cookie_file) - else: - self.login(mcj, cookie_file) - - def login(self, mcj, cookie_file): - self.session.add_handler(HTTPCookieProcessor(mcj)) + self.login(mcj) + except FileNotFoundError: + self.login(mcj) - form_data = {"username": self.config['username'], "password": self.config['password']} - # so we first encode keys to cp1251 then do default decode whole string - data_encoded = urlencode({k: v.encode('cp1251') for k, v in form_data.items()}).encode() - - self._catch_error_request(self.url + '/takelogin.php', data_encoded) - if 'uid' not in [cookie.name for cookie in mcj]: - logging.warning("we not authorized, please check your credentials") - else: - mcj.save(cookie_file, ignore_discard=True, ignore_expires=True) - logging.info('We successfully authorized') - - def draw(self, html: str): - torrents = re.findall(r'nam">(.*?)' - r'.+?s\'>.+?s\'>(.*?)<.+?sl_s\'>(\d+)<.+?sl_p\'>(\d+)<.+?s\'>(.*?)', html, re.S) - today, yesterday = time.strftime("%y.%m.%d"), time.strftime("%y.%m.%d", time.localtime(time.time()-86400)) - for tor in torrents: - torrent_date = "" - if self.config['torrentDate']: - ct = tor[5].split()[0] - if "сегодня" in ct: - torrent_date = today - elif "вчера" in ct: - # yeah this is yesterday - torrent_date = yesterday - else: - torrent_date = time.strftime("%y.%m.%d", time.strptime(ct, "%d.%m.%Y")) - torrent_date = f'[{torrent_date}] ' - torrent = {"engine_url": self.url, - "desc_link": self.url + tor[0], - "name": torrent_date + tor[1], - "link": "http://dl.kinozal.tv/download.php?id=" + tor[0].split("=")[1], - "size": self.units_convert(tor[2]), - "seeds": tor[3], - "leech": tor[4]} - - prettyPrinter(torrent) - del torrents - # return len(torrents) - - def path_to(self, *file): - return os.path.abspath(os.path.join(os.path.dirname(__file__), *file)) + def search(self, what, cat='all'): + if self.error: + self.pretty_error(what) + return + query = PATTERNS[2] % (self.url, what.replace(" ", "+"), + self.supported_categories[cat]) - @staticmethod - def units_convert(unit): - # replace size units - find = unit.split()[1] - replace = {'ТБ': 'TB', 'ГБ': 'GB', 'МБ': 'MB', 'КБ': 'KB'}[find] + # make first request (maybe it enough) + t0, total = time.time(), self.searching(query, True) + if self.error: + self.pretty_error(what) + return + # do async requests + if total > 50: + qrs = [PATTERNS[3] % (query, x) for x in rng(total)] + with ThreadPoolExecutor(len(qrs)) as executor: + executor.map(self.searching, qrs, timeout=30) - return unit.replace(find, replace) + logger.debug(f"--- {time.time() - t0} seconds ---") + logger.info(f"Found torrents: {total}") def download_torrent(self, url: str): - if self.blocked: - return # choose download method - if self.config.get("magnet"): - res = self._catch_error_request(self.url + "/get_srv_details.php?action=2&id=" + url.split("=")[1]) - # magnet = re.search(":\s([A-Z0-9]{40})<", res.read().decode())[1] - magnet = 'magnet:?xt=urn:btih:' + res.read().decode()[18:58] - # return magnet link - logging.debug(magnet + " " + url) - print(magnet + " " + url) + if config.get("magnet"): + url = f"{self.url}get_srv_details.php?" \ + f"action=2&id={url.split('=')[1]}" + + res = self._catch_error_request(url) + if self.error: + self.pretty_error(url) + return + + if config.get("magnet"): + path = 'magnet:?xt=urn:btih:' + res.read().decode()[18:58] else: # Create a torrent file file, path = tempfile.mkstemp('.torrent') - file = os.fdopen(file, "wb") + with os.fdopen(file, "wb") as fd: + # Write it to a file + fd.write(res.read()) - # Download url - response = self._catch_error_request(url) + # return magnet link / file path + logger.debug(path + " " + url) + print(path + " " + url) - # Write it to a file - file.write(response.read()) - file.close() + def login(self, mcj): + if self.error: + return + self.session.add_handler(HTTPCookieProcessor(mcj)) - # return file path - logging.debug(path + " " + url) - print(path + " " + url) + form_data = {"username": config['username'], + "password": config['password']} + logger.debug(f"Login. Data before: {form_data}") + # so we first encode vals to cp1251 then do default decode whole string + data_encoded = urlencode( + {k: v.encode('cp1251') for k, v in form_data.items()}).encode() + logger.debug(f"Login. Data after: {data_encoded}") + + self._catch_error_request(self.url + 'takelogin.php', data_encoded) + if self.error: + return + logger.debug(f"That we have: {[cookie for cookie in mcj]}") + if 'uid' in [cookie.name for cookie in mcj]: + 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, first=False): response = self._catch_error_request(query) + if not response: + return None page = response.read().decode('cp1251') self.draw(page) - total = int(re.search(r'Найдено\s+?(\d+)\s+?раздач', page)[1]) if first else -1 - - return total - def search(self, what, cat='all'): - if self.blocked: - return - query = f'{self.url}/browse.php?s={what.replace(" ", "+")}&c={self.supported_categories[cat]}' + return int(re.search(PATTERNS[0], page)[1]) if first else -1 - # make first request (maybe it enough) - total = self.searching(query, True) - # do async requests - if total > 50: - tasks = [] - for x in range(1, -(-total//50)): - task = threading.Thread(target=self.searching, args=(query + f"&page={x}",)) - tasks.append(task) - task.start() - - # wait slower request in stack - for task in tasks: - task.join() - del tasks + def draw(self, html: str): + torrents = re.findall(PATTERNS[1], html, re.S) + _part = partial(time.strftime, "%y.%m.%d") + # yeah this is yesterday + yesterday = _part(time.localtime(time.time() - 86400)) + for tor in torrents: + torrent_date = "" + if config['torrentDate']: + ct = tor[5].split()[0] + if "сегодня" in ct: + torrent_date = _part() + elif "вчера" in ct: + torrent_date = yesterday + else: + torrent_date = _part(time.strptime(ct, "%d.%m.%Y")) + torrent_date = f'[{torrent_date}] ' - logging.debug(f"--- {time.time() - start_time} seconds ---") - logging.info(f"Found torrents: {total}") + # replace size units + table = {'Т': 'T', 'Г': 'G', 'М': 'M', 'К': 'K', 'Б': 'B'} + + prettyPrinter({ + "engine_url": self.url, + "desc_link": self.url + tor[0], + "name": torrent_date + unescape(tor[1]), + "link": "http://dl.kinozal.tv/download.php?id=" + + tor[0].split("=")[1], + "size": tor[2].translate(tor[2].maketrans(table)), + "seeds": tor[3], + "leech": tor[4] + }) + del torrents - def _catch_error_request(self, url='', data=None): + def _catch_error_request(self, url='', data=None, retrieve=False): url = url or self.url try: - response = self.session.open(url, data) - # Only continue if response status is OK. - if response.getcode() != 200: - logging.error('Unable connect') - raise HTTPError(response.geturl(), response.getcode(), - f"HTTP request to {url} failed with status: {response.getcode()}", - response.info(), None) - except (URLError, HTTPError) as e: - logging.error(e) - raise e - - # checking that tracker is'nt blocked - self.blocked = False - if self.url not in response.geturl(): - logging.warning(f"{self.url} is blocked. Try proxy or another proxy") - self.blocked = True - - return response + response = self.session.open(url, data, 5) + # checking that tracker is'nt blocked + if self.url not in response.geturl(): + raise URLError(f"{self.url} is blocked. Try another proxy.") + except (socket.error, socket.timeout) as err: + if not retrieve: + return self._catch_error_request(url, data, True) + logger.error(err) + self.error = f"{self.url} is not response! Maybe it is blocked." + if "no host given" in err.args: + self.error = "Proxy is bad, try another!" + except (URLError, HTTPError) as err: + logger.error(err.reason) + self.error = err.reason + if hasattr(err, 'code'): + self.error = f"Request to {url} failed with status: {err.code}" + else: + return response + + return None + + def pretty_error(self, what): + 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 if __name__ == "__main__": - # benchmark start - start_time = time.time() - kinozal_se = kinozal() - kinozal_se.search('doctor') - print("--- %s seconds ---" % (time.time() - start_time)) - # benchmark end + engine = kinozal() + engine.search('doctor') diff --git a/nnmclub.ico b/nnmclub.ico deleted file mode 100644 index c25690734fb4e9380bf1b1b0be161410d217e640..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmb7CT}YEr7=F&TP3N>VbG0;S>mn2(u_!cxbVGt7FHH2J@**gSE}|e-1XIF7f^x+Q zAqq`z{237hS!Q5R5Lk-cgjz(H7sf8cnU4KEyVi1pWF=9Wi2&(M&!)*40) zn?m=H;4GNv0r?p89>M+p`0Xn>1(XfnM4WLjIc6uLwLE@eW1DCUg0z)dn;D)iH|Wd9 zH&+@Oh0|*}CTle^B6}wa8Cz&d)}1T~wBANv_cgq+YAE?zQP_P}yIK>ERvVRGD}OU1ZYAv1&z48Rs0p`Mqhl zTnUWSX^31hf+J4R-4)ybf5==kM;o`}^XP+oerP{PPFOipgLsDVPapInN;_fAx4?ZM z1I-tm2>N|^>#abaI};7Mz+_Mrp3~wM~;L}3P>G?Gd^}Y&o6UAU`2wZFh&t<8e zHF|ZKx=@U^Px39lI;Fo=V3850*Wt&mEHrdyBRby#jvqrpy#M?oscKmiimpm;Lvlaf zGZ;vvU(IJL?4BwGzYbwuIP+>g)mrvH9CRR@V?jYG?P@+=9&|^XOL&nUbi*J}0xUsE QY-Z4HScX1tS%zQYZ=vuTcK!~P$2kGQf!OSh@w$m<2)C;ADxj}E*i#%p`#5uEt{&L`6>zWya8v5w zdtfgQd@upZCLpq`xj!crU~1Q|%hOT;7%?JyAPxgF4+F3MWvmsLBgec>LV*xGZE?zl>W3XuuX= zc|F>uu%;adLu4^N|3Ms`kE=f9oPbCIQ7EK#aB+S&pz6~szqtU%>u{(Otb}Go?a|pFCkc)a#J+`NhV6D^lrTdC1yuop z6hyZov;hfDtQvv+6B^uT^P-=i`6c)kbP%T76>RtmR|uR0stz{)C0s=2CGe2&5nsNW zV1l5_i*tn5pFfvo(NAUBa6#xmKJ?wwrs%o8qA9qze(zU1BH z&hLj^?|j!Z5AJ;MU5`?~t_+X#HJrc6uk@S|eC9npF*|-KCdQZ)n34QiL|vIJYcn*} zM=uf$Z31mNy*6-+35$qHUUNa(NGXR-qsohVY&MxJTT0TIrA0kEWUTX#G^vbj-h5*; zHS<7G+A4xd3v+0o&7`u()wkl$&CfiL)7Lv8+8x9d=@xB^-jK47J2Ke$zNt`~V2ybC zyQ}8*0`9lBjZc1^K6byy**u!MH$>0<;;B}bwzR)F_2|MLJ1k!PrMcl=_jbeA z<-u!DblpwgVKo{yZ8aY)Yi+UHS9b_by}DGFJ)Gzmbd;CROw{zE@2B`e;iA4gnmoa# zEwovRZC0gukCg(FctwhwSI8A9c}cu7DOtIE*%Dr%(.+?).+?href="(d.+?)".+?/u>\s' + r'(.+?)<.+?b>(\d+)(\d+)<.+?(\d+)', + '%stracker.php?nm=%s&%s', "%s&start=%s", r'code"\svalue="(.+?)"') + +FILENAME = __file__[__file__.rfind('/') + 1:-3] +FILE_J, FILE_C = [path_to(FILENAME + fe) for fe in ['.json', '.cookie']] + +# base64 encoded image +ICON = ("AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaQicAXRQFADICAQAHAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADz4QA8PizAP" + "u3XQDpjEIBtgkCABoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "BAIAEuyUAP3/8AD//akA//+hAP92SgCVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFA" + "AAAAAAAAAAAAAAAAEAADjLiQD8//wA//7RFP//+lX/WlsPlwAAAAMAAAAGAAAAAAAAAAAA" + "AAAAEAgAQqNBAP99HADfIAYAfgAAABQAAAAX21UC///4AP///Sj/+/Z//lZcMJOOjQCrqI" + "EAwQ4CADAAAAAAAAAAAGEXAM39oAD//7oA/9ucAP94GwDFVRkK6p0wAP//owD/+KoB/+FT" + "C///uQD//+wA//67AP6QUQC9DggAGAAAAACPNQDl964A//qqAv//3AD//8sB/39WAP85Aw" + "X/nxkA/5MQAP/sJQD/0T8A//Z9AP/6kwD/86AA/qJGALwTAABEtzcA5cshAP/jOAD//7wg" + "///+Dv/RUQH/AgEE8hcAAG40BgB3RAAAzlYCAPh0BAD/zh8A//+RAP//hQD/5B8A/xcAAE" + "x+HgDXz5oc/8yfPv//2g7/6VMA/AkEABQAAAAAAAAAAQAAAA4cCgBBOwkAg3EfAKyPfQDE" + "dkAAq0ELAGYAAAAABQMBQNldFf3/8w3///sA/7AoAPIAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAchNAPLaLgD/+8AA//eOAP9qDAGpAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFwLgCX0h8A//WiAP/+TQD/Kg" + "QAZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALQwAZqgR" + "APr0hwD/2VIA/QAAAAYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAoBACp6BAD/7H0A/3ZlALoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAARAQAx4zcA/93AAPQAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACgEASawXAPMTCgAnAAAAAAAAAAAAAAAA" + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/D+sQfgfrEH4H6xBuAesQQ" + "ADrEEAAaxBAACsQQAArEEBAKxBg/+sQQP/rEED/6xBg/+sQYf/rEGH/6xBj/+sQQ==") + +# setup logging +logging.basicConfig( + format="%(asctime)s %(name)-12s %(levelname)-8s %(message)s", + datefmt="%m-%d %H:%M") +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + +try: + # try to load user data from file + with open(FILE_J, 'r+') as f: + cfg = json.load(f) + if "version" not in cfg.keys(): + cfg.update({"version": 2, "torrentDate": True}) + f.seek(0) + f.write(json.dumps(cfg, indent=4, sort_keys=False)) + f.truncate() + config = cfg + logger.debug("Config is loaded.") +except OSError as e: + logger.error(e) + # if file doesn't exist, we'll create it + with open(FILE_J, 'w') as f: + f.write(json.dumps(config, indent=4, sort_keys=False)) + # also write/rewrite ico file + with open(path_to(FILENAME + '.ico'), 'wb') as f: + f.write(base64.b64decode(ICON)) + logger.debug("Write files.") + class nnmclub(object): name = 'NoNaMe-Club' @@ -29,200 +116,181 @@ class nnmclub(object): 'anime': '24', 'software': '21'} - # default config for nnmclub.json - config = { - "version": 2, - "torrentDate": True, - "username": "USERNAME", - "password": "PASSWORD", - "proxy": False, - "proxies": { - "http": "", - "https": "" - }, - "ua": "Mozilla/5.0 (X11; Linux i686; rv:38.0) Gecko/20100101 Firefox/38.0" - } - def __init__(self): - # setup logging into qBittorrent/logs - logging.basicConfig(handlers=[logging.FileHandler(self.path_to('../../logs', 'nnmclub.log'), 'w', 'utf-8')], - level=logging.DEBUG, - format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', - datefmt='%m-%d %H:%M') - - try: - # try to load user data from file - with open(self.path_to('nnmclub.json'), 'r+') as f: - config = json.load(f) - if "version" not in config.keys(): - config.update({"version": 2, "torrentDate": True}) - f.seek(0) - f.write(json.dumps(config, indent=4, sort_keys=False)) - f.truncate() - self.config = config - except OSError as e: - logging.error(e) - # if file doesn't exist, we'll create it - with open(self.path_to('nnmclub.json'), 'w') as f: - f.write(json.dumps(self.config, indent=4, sort_keys=False)) + # error message + self.error = None # establish connection self.session = build_opener() # add proxy handler if needed - if self.config['proxy'] and any(self.config['proxies'].keys()): - self.session.add_handler(ProxyHandler(self.config['proxies'])) + 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.pop() - self.session.addheaders.append(('User-Agent', self.config['ua'])) + self.session.addheaders.append(('User-Agent', config['ua'])) - # avoid endless waiting - self.blocked = False - - mcj = MozillaCookieJar() - cookie_file = self.path_to('nnmclub.cookie') # load local cookies - if os.path.isfile(cookie_file): - mcj.load(cookie_file, ignore_discard=True) + mcj = MozillaCookieJar() + try: + mcj.load(FILE_C, ignore_discard=True) if 'phpbb2mysql_4_sid' in [cookie.name for cookie in mcj]: # if cookie.expires < int(time.time()) - logging.info("Local cookies is loaded") + logger.info("Local cookies is loaded") self.session.add_handler(HTTPCookieProcessor(mcj)) else: - logging.info("Local cookies expired or bad") - logging.debug(f"That we have: {[cookie for cookie in mcj]}") + logger.info("Local cookies expired or bad") + logger.debug(f"That we have: {[cookie for cookie in mcj]}") mcj.clear() - self.login(mcj, cookie_file) - else: - self.login(mcj, cookie_file) + self.login(mcj) + except FileNotFoundError: + self.login(mcj) + + def search(self, what, cat='all'): + if self.error: + self.pretty_error(what) + return + c = self.supported_categories[cat] + query = PATTERNS[2] % (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 + # do async requests + if total > 50: + qrs = [PATTERNS[3] % (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): + # Download url + response = self._catch_error_request(url) + if self.error: + self.pretty_error(url) + return + + # Create a torrent file + file, path = tempfile.mkstemp('.torrent') + with os.fdopen(file, "wb") as fd: + # Write it to a file + fd.write(response.read()) - def login(self, mcj, cookie_file): + # return file path + logger.debug(path + " " + url) + print(path + " " + url) + + def login(self, mcj): + if self.error: + return # if we wanna use https we mast add ssl=enable_ssl to cookie - mcj.set_cookie(Cookie(0, 'ssl', "enable_ssl", None, False, '.nnmclub.to', True, - False, '/', True, False, None, 'ParserCookie', None, None, None)) + mcj.set_cookie(Cookie(0, 'ssl', "enable_ssl", None, False, + '.nnmclub.to', True, False, '/', True, + False, None, 'ParserCookie', None, None, None)) self.session.add_handler(HTTPCookieProcessor(mcj)) response = self._catch_error_request(self.url + 'login.php') - if not self.blocked: - code = re.search(r'code"\svalue="(.+?)"', response.read().decode('cp1251'))[1] - form_data = {"username": self.config['username'], - "password": self.config['password'], - "autologin": "on", - "code": code, - "login": "Вход"} - # so we first encode keys to cp1251 then do default decode whole string - data_encoded = urlencode({k: v.encode('cp1251') for k, v in form_data.items()}).encode() - - self._catch_error_request(self.url + 'login.php', data_encoded) - if 'phpbb2mysql_4_sid' not in [cookie.name for cookie in mcj]: - logging.warning("we not authorized, please check your credentials") - else: - mcj.save(cookie_file, ignore_discard=True, ignore_expires=True) - logging.info('We successfully authorized') + if not response: + return None + code = re.search(PATTERNS[4], response.read().decode('cp1251'))[1] + form_data = {"username": config['username'], + "password": config['password'], + "autologin": "on", + "code": code, + "login": "Вход"} + # so we first encode vals to cp1251 then do default decode whole string + data_encoded = urlencode( + {k: v.encode('cp1251') for k, v in form_data.items()}).encode() + + self._catch_error_request(self.url + 'login.php', data_encoded) + if self.error: + return + logger.debug(f"That we have: {[cookie for cookie in mcj]}") + if 'phpbb2mysql_4_sid' in [cookie.name for cookie in mcj]: + 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 draw(self, html: str): - torrents = re.findall(r'd\stopic.+?href="(.+?)".+?(.+?).+?href="(d.+?)"' - r'.+?/u>\s(.+?)<.+?b>(\d+)(\d+)<.+?(\d+)', html, re.S) + torrents = re.findall(PATTERNS[1], html, re.S) for tor in torrents: torrent_date = "" - if self.config['torrentDate']: - torrent_date = f'[{time.strftime("%y.%m.%d", time.localtime(int(tor[6])))}] ' - torrent = {"engine_url": self.url, - "desc_link": self.url + tor[0], - "name": torrent_date + tor[1], - "link": self.url + tor[2], - "size": tor[3].replace(',', '.'), - "seeds": tor[4], - "leech": tor[5]} - - prettyPrinter(torrent) + if config['torrentDate']: + _loc = time.localtime(int(tor[6])) + torrent_date = f'[{time.strftime("%y.%m.%d", _loc)}] ' + + prettyPrinter({ + "engine_url": self.url, + "desc_link": self.url + tor[0], + "name": torrent_date + tor[1], + "link": self.url + tor[2], + "size": tor[3].replace(',', '.'), + "seeds": tor[4], + "leech": tor[5] + }) del torrents - # return len(torrents) - - def path_to(self, *file): - return os.path.abspath(os.path.join(os.path.dirname(__file__), *file)) - - def download_torrent(self, url): - if self.blocked: - return - # Create a torrent file - file, path = tempfile.mkstemp('.torrent') - file = os.fdopen(file, "wb") - - # Download url - response = self._catch_error_request(url) - - # Write it to a file - file.write(response.read()) - file.close() - - # return file path - logging.debug(path + " " + url) - print(path + " " + url) def searching(self, query, first=False): response = self._catch_error_request(query) + if not response: + return None page = response.read().decode('cp1251') self.draw(page) - total = int(re.search(r'(\d{1,3})\s\(max:', page)[1]) if first else -1 - - return total - def search(self, what, cat='all'): - if self.blocked: - return - c = self.supported_categories[cat] - query = f'{self.url}tracker.php?nm={what.replace(" ", "+")}&{"f=-1" if c == "-1" else "c=" + c}' + return int(re.search(PATTERNS[0], page)[1]) if first else -1 - # make first request (maybe it enough) - total = self.searching(query, True) - # do async requests - if total > 50: - tasks = [] - for x in range(1, -(-total//50)): - task = threading.Thread(target=self.searching, args=(query + f"&start={x * 50}",)) - tasks.append(task) - task.start() + def _catch_error_request(self, url='', data=None, retrieve=False): + url = url or self.url - # wait slower request in stack - for task in tasks: - task.join() - del tasks + try: + response = self.session.open(url, data, 5) + # checking that tracker is'nt blocked + if not any([x in response.geturl() + # redirect to nnm-club.ws on download + for x in [self.url, 'nnm-club.ws']]): + raise URLError(f"{self.url} is blocked. Try another proxy.") + except (socket.error, socket.timeout) as err: + if not retrieve: + return self._catch_error_request(url, data, True) + logger.error(err) + self.error = f"{self.url} is not response! Maybe it is blocked." + if "no host given" in err.args: + self.error = "Proxy is bad, try another!" + except (URLError, HTTPError) as err: + logger.error(err.reason) + self.error = err.reason + if hasattr(err, 'code'): + self.error = f"Request to {url} failed with status: {err.code}" + else: + return response - logging.debug(f"--- {time.time() - start_time} seconds ---") - logging.info(f"Found torrents: {total}") + return None - def _catch_error_request(self, url='', data=None): - url = url or self.url + def pretty_error(self, what): + 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}) - try: - response = self.session.open(url, data) - # Only continue if response status is OK. - if response.getcode() != 200: - logging.error('Unable connect') - raise HTTPError(response.geturl(), response.getcode(), - f"HTTP request to {url} failed with status: {response.getcode()}", - response.info(), None) - except (URLError, HTTPError) as e: - logging.error(e) - raise e - - # checking that tracker is'nt blocked - self.blocked = False - if self.url not in response.geturl(): - print(response.geturl()) - logging.warning(f"{self.url} is blocked. Try proxy or another proxy") - self.blocked = True - - return response + self.error = None if __name__ == "__main__": - # benchmark start - start_time = time.time() - # nnmclub_se = nnmclub() - # nnmclub_se.search('bird') - print(f"--- {time.time() - start_time} seconds ---") - # benchmark end + engine = nnmclub() + engine.search('doctor') diff --git a/rutracker.py b/rutracker.py new file mode 100644 index 0000000..0ef439e --- /dev/null +++ b/rutracker.py @@ -0,0 +1,280 @@ +# VERSION: 1.0 +# AUTHORS: imDMG [imdmgg@gmail.com] + +# rutracker.org search engine plugin for qBittorrent + +import base64 +import json +import logging +import os +import re +import socket +import tempfile +import time + +from concurrent.futures import ThreadPoolExecutor +from html import unescape +from http.cookiejar import Cookie, MozillaCookieJar +from urllib.error import URLError, HTTPError +from urllib.parse import urlencode, unquote +from urllib.request import build_opener, HTTPCookieProcessor, ProxyHandler + +from novaprinter import prettyPrinter + +# default config +config = { + "version": 2, + "torrentDate": True, + "username": "USERNAME", + "password": "PASSWORD", + "proxy": False, + "proxies": { + "http": "", + "https": "" + }, + "ua": "Mozilla/5.0 (X11; Linux i686; rv:38.0) Gecko/20100101 Firefox/38.0 " +} + + +def path_to(*file): + return os.path.abspath(os.path.join(os.path.dirname(__file__), *file)) + + +def rng(t): + return range(50, -(-t // 50) * 50, 50) + + +PATTERNS = (r'(\d{1,3})\s(.+?)(.+?)\s&.+?data-ts_text="(.+?)">.+?Личи">(\d+) 50: + qrs = [PATTERNS[3] % (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): + # Download url + response = self._catch_error_request(url) + if self.error: + self.pretty_error(url) + return + + # Create a torrent file + file, path = tempfile.mkstemp('.torrent') + with os.fdopen(file, "wb") as fd: + # Write it to a file + fd.write(response.read()) + + # return file path + logger.debug(path + " " + url) + print(path + " " + url) + + def login(self, mcj): + if self.error: + return + # if we wanna use https we mast add ssl=enable_ssl to cookie + mcj.set_cookie(Cookie(0, 'ssl', "enable_ssl", None, False, + '.rutracker.org', True, False, '/', True, + False, None, 'ParserCookie', None, None, None)) + self.session.add_handler(HTTPCookieProcessor(mcj)) + + form_data = {"login_username": config['username'], + "login_password": config['password'], + "login": "вход"} + logger.debug(f"Login. Data before: {form_data}") + # so we first encode vals to cp1251 then do default decode whole string + data_encoded = urlencode( + {k: v.encode('cp1251') for k, v in form_data.items()}).encode() + logger.debug(f"Login. Data after: {data_encoded}") + self._catch_error_request(self.url + 'login.php', data_encoded) + if self.error: + return + logger.debug(f"That we have: {[cookie for cookie in mcj]}") + if 'bb_session' in [cookie.name for cookie in mcj]: + 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, first=False): + response = self._catch_error_request(query) + if not response: + return None + page = response.read().decode('cp1251') + self.draw(page) + + return int(re.search(PATTERNS[0], page)[1]) if first else -1 + + def draw(self, html: str): + torrents = re.findall(PATTERNS[1], html, re.S) + for tor in torrents: + local = time.strftime("%y.%m.%d", time.localtime(int(tor[6]))) + torrent_date = f"[{local}] " if config['torrentDate'] else "" + + prettyPrinter({ + "engine_url": self.url, + "desc_link": self.url + tor[0], + "name": torrent_date + unescape(tor[1]), + "link": self.url + tor[2], + "size": unescape(tor[3]), + "seeds": tor[4] if tor[4].isdigit() else '0', + "leech": tor[5] + }) + del torrents + + def _catch_error_request(self, url='', data=None, retrieve=False): + url = url or self.url + + try: + response = self.session.open(url, data, 5) + # checking that tracker is'nt blocked + if self.url not in response.geturl(): + raise URLError(f"{self.url} is blocked. Try another proxy.") + except (socket.error, socket.timeout) as err: + if not retrieve: + return self._catch_error_request(url, data, True) + logger.error(err) + self.error = f"{self.url} is not response! Maybe it is blocked." + if "no host given" in err.args: + self.error = "Proxy is bad, try another!" + except (URLError, HTTPError) as err: + logger.error(err.reason) + self.error = err.reason + if hasattr(err, 'code'): + self.error = f"Request to {url} failed with status: {err.code}" + else: + return response + + return None + + def pretty_error(self, what): + 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 + + +if __name__ == "__main__": + engine = rutracker() + engine.search('doctor')