diff --git a/.travis.yml b/.travis.yml index ab2843b..9885159 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,23 @@ language: python -dist: trusty +dist: bionic sudo: false cache: pip matrix: include: - - python: 2.7 - env: NOVA_DIR=nova - - python: 3.4 - env: NOVA_DIR=nova3 + - python: 3.5 + - python: 3.6 + - python: 3.7 + - python: 3.8 - python: nightly - env: NOVA_DIR=nova3 install: - pip install pycodestyle pyflakes script: - - cd "$TRAVIS_BUILD_DIR/$NOVA_DIR/engines" + - cd "$TRAVIS_BUILD_DIR/nova3/engines" - python -m compileall *.py - pyflakes *.py # skipping E265, fixing it will break plugin usage on older qbt instances (< v4.1.2) diff --git a/README.md b/README.md index 426569d..36961a3 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,11 @@ Search Plugins [![TravisCI Status](https://travis-ci.org/qbittorrent/search-plugins.svg?branch=master)](https://travis-ci.org/qbittorrent/search-plugins) -This repository contains search plugins for the search feature in qBittorrent. +This repository contains search plugins for the search feature in [qBittorrent](https://github.com/qbittorrent/qBittorrent). -:warning: We are going to remove support for Python2 soon. Please upgrade to Python3 to be prepared and get a better experience. +:warning: We removed support for Python 2. Please, upgrade to Python 3 to continue using the search function. + +Jackett search plugin is enabled by default but you have to install an external program to make it work. You can disable the Jackett search plugin or [install Jackett](https://github.com/qbittorrent/search-plugins/wiki/How-to-configure-Jackett-plugin). Most probably, you want to head over to the [wiki](https://github.com/qbittorrent/search-plugins/wiki): * [List of unofficial search plugins](https://github.com/qbittorrent/search-plugins/wiki/Unofficial-search-plugins) diff --git a/nova/engines/__init__.py b/nova/engines/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nova/engines/eztv.py b/nova/engines/eztv.py deleted file mode 100644 index d6fa49e..0000000 --- a/nova/engines/eztv.py +++ /dev/null @@ -1,79 +0,0 @@ -#VERSION: 1.10 -# AUTHORS: nindogo -# CONTRIBUTORS: Diego de las Heras (ngosang@hotmail.es) - -try: - # python3 - from html.parser import HTMLParser -except ImportError: - # python2 - from HTMLParser import HTMLParser - -# qBt -from novaprinter import prettyPrinter -from helpers import retrieve_url - - -class eztv(object): - name = "EZTV" - url = 'https://eztv.io' - supported_categories = {'all': 'all', 'tv': 'tv'} - - class MyHtmlParser(HTMLParser): - A, TD, TR, TABLE = ('a', 'td', 'tr', 'table') - - """ Sub-class for parsing results """ - def __init__(self, url): - HTMLParser.__init__(self) - self.url = url - - self.in_table_row = False - self.current_item = {} - - def handle_starttag(self, tag, attrs): - params = dict(attrs) - - if (params.get('class') == 'forum_header_border' - and params.get('name') == 'hover'): - self.in_table_row = True - self.current_item = {} - self.current_item['seeds'] = -1 - self.current_item['leech'] = -1 - self.current_item['size'] = -1 - self.current_item['engine_url'] = self.url - - if (tag == self.A - and self.in_table_row and params.get('class') == 'magnet'): - self.current_item['link'] = params.get('href') - - if (tag == self.A - and self.in_table_row and params.get('class') == 'epinfo'): - self.current_item['desc_link'] = self.url + params.get('href') - self.current_item['name'] = params.get('title').split(' (')[0] - - def handle_data(self, data): - data = data.replace(',', '') - if (self.in_table_row - and (data.endswith(' KB') or data.endswith(' MB') or data.endswith(' GB'))): - self.current_item['size'] = data - - elif self.in_table_row and data.isnumeric(): - self.current_item['seeds'] = int(data) - - def handle_endtag(self, tag): - if self.in_table_row and tag == self.TR: - prettyPrinter(self.current_item) - self.in_table_row = False - - def search(self, what, cat='all'): - query = self.url + '/search/' + what.replace('%20', '-') - eztv_html = retrieve_url(query) - - eztv_parser = self.MyHtmlParser(self.url) - eztv_parser.feed(eztv_html) - eztv_parser.close() - - -if __name__ == '__main__': - eztv_se = eztv() - eztv_se.search('Acre', 'all') diff --git a/nova/engines/jackett.py b/nova/engines/jackett.py deleted file mode 100644 index 6d29ff4..0000000 --- a/nova/engines/jackett.py +++ /dev/null @@ -1,205 +0,0 @@ -#VERSION: 3.4 -# AUTHORS: Diego de las Heras (ngosang@hotmail.es) -# CONTRIBUTORS: ukharley -# hannsen (github.com/hannsen) - -import json -import os -import xml.etree.ElementTree -try: - # python3 - from urllib.parse import urlencode, unquote - from urllib import request as urllib_request - from http.cookiejar import CookieJar -except ImportError: - # python2 - from urllib import urlencode, unquote - import urllib2 as urllib_request - from cookielib import CookieJar - -# qBt -from novaprinter import prettyPrinter -from helpers import download_file - - -############################################################################### -# load configuration from file -CONFIG_FILE = 'jackett.json' -CONFIG_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), CONFIG_FILE) -CONFIG_DATA = { - 'api_key': 'YOUR_API_KEY_HERE', # jackett api - 'tracker_first': False, # (False/True) add tracker name to beginning of search result - 'url': 'http://127.0.0.1:9117', # jackett url -} - - -def load_configuration(): - global CONFIG_PATH, CONFIG_DATA - try: - # try to load user data from file - with open(CONFIG_PATH) as f: - CONFIG_DATA = json.load(f) - except ValueError: - # if file exists but it's malformed we load add a flag - CONFIG_DATA['malformed'] = True - except Exception: - # if file doesn't exist, we create it - with open(CONFIG_PATH, 'w') as f: - f.write(json.dumps(CONFIG_DATA, indent=4, sort_keys=True)) - - # do some checks - if any(item not in CONFIG_DATA for item in ['api_key', 'tracker_first', 'url']): - CONFIG_DATA['malformed'] = True - - -load_configuration() -############################################################################### - - -class jackett(object): - name = 'Jackett' - url = CONFIG_DATA['url'] if CONFIG_DATA['url'][-1] != '/' else CONFIG_DATA['url'][:-1] - api_key = CONFIG_DATA['api_key'] - supported_categories = { - 'all': None, - 'anime': ['5070'], - 'books': ['8000'], - 'games': ['1000', '4000'], - 'movies': ['2000'], - 'music': ['3000'], - 'software': ['4000'], - 'tv': ['5000'], - } - - def download_torrent(self, download_url): - # fix for some indexers with magnet link inside .torrent file - if download_url.startswith('magnet:?'): - print(download_url + " " + download_url) - response = self.get_response(download_url) - if response is not None and response.startswith('magnet:?'): - print(response + " " + download_url) - else: - print(download_file(download_url)) - - def search(self, what, cat='all'): - what = unquote(what) - category = self.supported_categories[cat.lower()] - - # check for malformed configuration - if 'malformed' in CONFIG_DATA: - self.handle_error("malformed configuration file", what) - return - - # check api_key - if self.api_key == "YOUR_API_KEY_HERE": - self.handle_error("api key error", what) - return - - # prepare jackett url - params = [ - ('apikey', self.api_key), - ('q', what) - ] - if category is not None: - params.append(('cat', ','.join(category))) - params = urlencode(params) - jacket_url = self.url + "/api/v2.0/indexers/all/results/torznab/api?%s" % params - response = self.get_response(jacket_url) - if response is None: - self.handle_error("connection error", what) - return - - # process search results - response_xml = xml.etree.ElementTree.fromstring(response) - for result in response_xml.find('channel').findall('item'): - res = {} - - title = result.find('title') - if title is not None: - title = title.text - else: - continue - - tracker = result.find('jackettindexer') - tracker = '' if tracker is None else tracker.text - if CONFIG_DATA['tracker_first']: - res['name'] = '[%s] %s' % (tracker, title) - else: - res['name'] = '%s [%s]' % (title, tracker) - - res['link'] = result.find(self.generate_xpath('magneturl')) - if res['link'] is not None: - res['link'] = res['link'].attrib['value'] - else: - res['link'] = result.find('link') - if res['link'] is not None: - res['link'] = res['link'].text - else: - continue - - res['size'] = result.find('size') - res['size'] = -1 if res['size'] is None else (res['size'].text + ' B') - - res['seeds'] = result.find(self.generate_xpath('seeders')) - res['seeds'] = -1 if res['seeds'] is None else int(res['seeds'].attrib['value']) - - res['leech'] = result.find(self.generate_xpath('peers')) - res['leech'] = -1 if res['leech'] is None else int(res['leech'].attrib['value']) - - if res['seeds'] != -1 and res['leech'] != -1: - res['leech'] -= res['seeds'] - - res['desc_link'] = result.find('comments') - if res['desc_link'] is not None: - res['desc_link'] = res['desc_link'].text - else: - res['desc_link'] = result.find('guid') - res['desc_link'] = '' if res['desc_link'] is None else res['desc_link'].text - - # note: engine_url can't be changed, torrent download stops working - res['engine_url'] = self.url - - prettyPrinter(self.escape_pipe(res)) - - def generate_xpath(self, tag): - return './{http://torznab.com/schemas/2015/feed}attr[@name="%s"]' % tag - - # Safety measure until it's fixed in prettyPrinter - def escape_pipe(self, dictionary): - for key in dictionary.keys(): - if isinstance(dictionary[key], str): - dictionary[key] = dictionary[key].replace('|', '%7C') - return dictionary - - def get_response(self, query): - response = None - try: - # we can't use helpers.retrieve_url because of redirects - # we need the cookie processor to handle redirects - opener = urllib_request.build_opener(urllib_request.HTTPCookieProcessor(CookieJar())) - response = opener.open(query).read().decode('utf-8') - except urllib_request.HTTPError as e: - # if the page returns a magnet redirect, used in download_torrent - if e.code == 302: - response = e.url - except Exception: - pass - return response - - def handle_error(self, error_msg, what): - # we need to print the search text to be displayed in qBittorrent when - # 'Torrent names only' is enabled - prettyPrinter({ - 'seeds': -1, - 'size': -1, - 'leech': -1, - 'engine_url': self.url, - 'link': self.url, - 'desc_link': 'https://github.com/qbittorrent/search-plugins/wiki/How-to-configure-Jackett-plugin', # noqa - 'name': "Jackett: %s! Right-click this row and select 'Open description page' to open help. Configuration file: '%s' Search: '%s'" % (error_msg, CONFIG_PATH, what) # noqa - }) - - -if __name__ == "__main__": - jackett_se = jackett() - jackett_se.search("ubuntu server", 'software') diff --git a/nova/engines/leetx.py b/nova/engines/leetx.py deleted file mode 100644 index 3380082..0000000 --- a/nova/engines/leetx.py +++ /dev/null @@ -1,170 +0,0 @@ -#VERSION: 2.2 -#AUTHORS: Vikas Yadav (https://github.com/v1k45 | http://v1k45.com) -#CONTRIBUTORS: Diego de las Heras (ngosang@hotmail.es) - -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of the author nor the names of its contributors may be -# used to endorse or promote products derived from this software without -# specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -import re -try: - # python3 - from html.parser import HTMLParser -except ImportError: - # python2 - from HTMLParser import HTMLParser - -# qBt -from helpers import retrieve_url -from novaprinter import prettyPrinter - - -class leetx(object): - url = "https://1337x.to" - name = "1337x" - supported_categories = { - 'all': 'All', - 'movies': 'Movies', - 'tv': 'TV', - 'music': 'Music', - 'games': 'Games', - 'anime': 'Anime', - 'software': 'Apps' - } - - class MyHtmlParser(HTMLParser): - A, TABLE, TR, TD, SPAN = ('a', 'table', 'tr', 'td', 'span') - - """ Sub-class for parsing results """ - def __init__(self, results, url): - HTMLParser.__init__(self) - self.results = results - self.url = url - - self.current_result = {} - self.current_item = None - self.inside_table = False - self.inside_row = False - - def handle_starttag(self, tag, attrs): - # are we inside the results table body or not - # if we are not inside the table, no need to process any further - self.inside_table = self.inside_table or tag == self.TABLE - if not self.inside_table: - return - - # convert attrs tuple to dictionary - attrs = dict(attrs) - - # for torrent name and link - link = attrs.get('href', '') - if tag == self.A and link.startswith('/torrent'): - self.current_result['link'] = self.url + link - self.current_result['desc_link'] = self.url + link - self.current_result['engine_url'] = self.url - self.current_item = 'name' - - # to ignore uploader name attached to the torrent size in span tag - if tag == self.SPAN: - self.current_item = None - - # if this is a there can be seeds, leeches or size inside it. - if tag == self.TD: - self.inside_row = True - - # find apporipate data key using class name of td - for item in ['seeds', 'leech', 'size']: - if item in attrs.get('class', ''): - self.current_item = item - break - - def handle_data(self, data): - # if we are not inside the table, no need to process any further - if not self.inside_table: - return - - # do not process data if we are not inside the table body - if self.current_item: - prev_value = self.current_result.get(self.current_item, '') - self.current_result[self.current_item] = prev_value + data - - def handle_endtag(self, tag): - # are we inside the results table body or not - # if we are not inside the table, no need to process any further - if tag == self.TABLE: - self.inside_table = False - if not self.inside_table: - return - - # exiting the table data and maybe moving td or tr element - if self.inside_row and tag == self.TD: - self.inside_row = False - self.current_item = None - - # exiting the tr element, which means all necessary data for a torrent has been - # extracted, we should save it and clean the object's state. - if self.current_result and tag == self.TR: - if 'size' in self.current_result: - self.current_result['size'] = self.current_result['size'].replace(',', '') - # skip malformed names (eg. with @) - if 'name' in self.current_result: - prettyPrinter(self.current_result) - self.results.append('a') - self.current_result = {} - self.current_item = None - - def download_torrent(self, download_url): - # since 1337x does not provide torrent links in the search results, - # we will have to fetch the page and extract the magnet link - torrent_page = retrieve_url(download_url) - magnet_match = re.search(r"href\s*\=\s*\"(magnet[^\"]+)\"", torrent_page) - if magnet_match and magnet_match.groups(): - print(magnet_match.groups()[0] + " " + download_url) - else: - raise Exception('Error, please fill a bug report!') - - def search(self, what, cat='all'): - cat = cat.lower() - - # decide which type of search to perform based on category - search_page = "search" if cat == 'all' else 'category-search' - search_url = "{url}/{search_page}/{search_query}/".format( - url=self.url, search_page=search_page, search_query=what) - - # apply search category to url, if any. - if cat != 'all': - search_url += self.supported_categories[cat] + "/" - - # try to get 15 pages (20 * 15 = 300 results) and stop when we don't found results - results_list = [] - parser = self.MyHtmlParser(results_list, self.url) - page = 1 - while page < 16: - # download the page - html = retrieve_url(search_url + str(page) + '/') - parser.feed(html) - if len(results_list) < 1: - break - del results_list[:] - page += 1 - parser.close() diff --git a/nova/engines/legittorrents.py b/nova/engines/legittorrents.py deleted file mode 100644 index 50480f4..0000000 --- a/nova/engines/legittorrents.py +++ /dev/null @@ -1,104 +0,0 @@ -#VERSION: 2.4 -# AUTHORS: Christophe Dumez (chris@qbittorrent.org) -# Douman (custparasite@gmx.se) - -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of the author nor the names of its contributors may be -# used to endorse or promote products derived from this software without -# specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from novaprinter import prettyPrinter -from helpers import retrieve_url, download_file -from HTMLParser import HTMLParser -from re import compile as re_compile - - -class legittorrents(object): - url = 'http://www.legittorrents.info' - name = 'Legit Torrents' - supported_categories = {'all': '0', 'movies': '1', 'tv': '13', - 'music': '2', 'games': '3', 'anime': '5', 'books': '6'} - - def download_torrent(self, info): - print(download_file(info)) - - class MyHtmlParseWithBlackJack(HTMLParser): - """ Parser class """ - def __init__(self, url): - HTMLParser.__init__(self) - self.url = url - self.current_item = None - self.save_item_key = None - - def handle_starttag(self, tag, attrs): - """ Parser's start tag handler """ - if self.current_item: - params = dict(attrs) - if tag == "a": - link = params["href"] - if link.startswith("index") and "title" in params: - # description link - self.current_item["name"] = params["title"][14:] - self.current_item["desc_link"] = "/".join((self.url, link)) - elif link.startswith("download"): - self.current_item["link"] = "/".join((self.url, link)) - elif tag == "td": - if ("width" in params and params["width"] == "30" - and "leech" not in self.current_item): - self.save_item_key = "leech" if "seeds" in self.current_item else "seeds" - - elif tag == "tr": - self.current_item = {} - self.current_item["size"] = "" - self.current_item["engine_url"] = self.url - - def handle_endtag(self, tag): - """ Parser's end tag handler """ - if self.current_item and tag == "tr": - if len(self.current_item) > 4: - prettyPrinter(self.current_item) - self.current_item = None - - def handle_data(self, data): - """ Parser's data handler """ - if self.save_item_key: - self.current_item[self.save_item_key] = data.strip() - self.save_item_key = None - - def search(self, what, cat='all'): - """ Performs search """ - query = "".join((self.url, "/index.php?page=torrents&search=", what, "&category=", - self.supported_categories.get(cat, '0'), "&active=1")) - - get_table = re_compile(r'(?s)(.*)') - data = get_table.search(retrieve_url(query)).group(0) - # extract first ten pages of next results - next_pages = re_compile('(?m)') - next_pages = ["".join((self.url, page)) for page in next_pages.findall(data)[:10]] - - parser = self.MyHtmlParseWithBlackJack(self.url) - parser.feed(data) - parser.close() - - for page in next_pages: - parser.feed(get_table.search(retrieve_url(page)).group(0)) - parser.close() diff --git a/nova/engines/limetorrents.py b/nova/engines/limetorrents.py deleted file mode 100644 index 387cffd..0000000 --- a/nova/engines/limetorrents.py +++ /dev/null @@ -1,133 +0,0 @@ -#VERSION: 4.5 -# AUTHORS: Lima66 -# CONTRIBUTORS: Diego de las Heras (ngosang@hotmail.es) - -import re -try: - # python3 - from html.parser import HTMLParser - from urllib.parse import quote -except ImportError: - # python2 - from HTMLParser import HTMLParser - from urllib import quote - -# qBt -from novaprinter import prettyPrinter -from helpers import retrieve_url - -# Fix invalid certificate in Windows -import ssl -ssl._create_default_https_context = ssl._create_unverified_context - - -class limetorrents(object): - url = "https://limetor.com" - name = "LimeTorrents" - supported_categories = {'all': 'all', - 'anime': 'anime', - 'software': 'applications', - 'games': 'games', - 'movies': 'movies', - 'music': 'music', - 'tv': 'tv'} - - class MyHtmlParser(HTMLParser): - """ Sub-class for parsing results """ - - def error(self, message): - pass - - A, TD, TR, HREF = ('a', 'td', 'tr', 'href') - - def __init__(self, url): - HTMLParser.__init__(self) - self.url = url - self.current_item = {} # dict for found item - self.item_name = None # key's name in current_item dict - self.page_empty = 22000 - self.inside_tr = False - self.findTable = False - self.parser_class = {"tdnormal": "size", # class - "tdseed": "seeds", - "tdleech": "leech"} - - def handle_starttag(self, tag, attrs): - - params = dict(attrs) - if params.get('class') == 'table2': - self.findTable = True - - if tag == self.TR and self.findTable and (params.get('bgcolor') == '#F4F4F4' or params.get('bgcolor') == '#FFFFFF'): # noqa - self.inside_tr = True - self.current_item = {} - if not self.inside_tr: - return - - if self.inside_tr and tag == self.TD: - if "class" in params: - self.item_name = self.parser_class.get(params["class"], None) - if self.item_name: - self.current_item[self.item_name] = -1 - - if self.inside_tr and tag == self.A and self.HREF in params: - link = params["href"] - if link.startswith("http://itorrents.org/torrent/"): - self.current_item["engine_url"] = self.url - self.item_name = "name" - elif link.endswith(".html"): - try: - safe_link = quote(self.url + link, safe='/:') - except KeyError: - safe_link = self.url + link - self.current_item["link"] = safe_link - self.current_item["desc_link"] = safe_link - - def handle_data(self, data): - if self.inside_tr and self.item_name: - if self.item_name == 'size' and (data.endswith('MB') or data.endswith('GB')): - self.current_item[self.item_name] = data.strip().replace(',', '') - elif not self.item_name == 'size': - self.current_item[self.item_name] = data.strip().replace(',', '') - - self.item_name = None - - def handle_endtag(self, tag): - if tag == 'table': - self.findTable = False - - if self.inside_tr and tag == self.TR: - self.inside_tr = False - self.item_name = None - array_length = len(self.current_item) - if array_length < 1: - return - prettyPrinter(self.current_item) - self.current_item = {} - - def download_torrent(self, info): - # since limetorrents provides torrent links in itorrent (cloudflare protected), - # we have to fetch the info page and extract the magnet link - info_page = retrieve_url(info) - magnet_match = re.search(r"href\s*\=\s*\"(magnet[^\"]+)\"", info_page) - if magnet_match and magnet_match.groups(): - print(magnet_match.groups()[0] + " " + info) - else: - raise Exception('Error, please fill a bug report!') - - def search(self, query, cat='all'): - """ Performs search """ - query = query.replace("%20", "-") - category = self.supported_categories[cat] - - parser = self.MyHtmlParser(self.url) - page = 1 - while True: - page_url = "{0}/search/{1}/{2}/seeds/{3}/".format(self.url, category, query, page) - html = retrieve_url(page_url) - lunghezza_html = len(html) - if page > 6 or lunghezza_html <= parser.page_empty: - return - parser.feed(html) - page += 1 - parser.close() diff --git a/nova/engines/piratebay.py b/nova/engines/piratebay.py deleted file mode 100644 index 765cef9..0000000 --- a/nova/engines/piratebay.py +++ /dev/null @@ -1,191 +0,0 @@ -#VERSION: 2.20 -# AUTHORS: Fabien Devaux (fab@gnux.info) -# CONTRIBUTORS: Christophe Dumez (chris@qbittorrent.org) -# Arthur (custparasite@gmx.se) -# Diego de las Heras (ngosang@hotmail.es) - -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of the author nor the names of its contributors may be -# used to endorse or promote products derived from this software without -# specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -try: - # python3 - from html.parser import HTMLParser -except ImportError: - # python2 - from HTMLParser import HTMLParser - -# qBt -from novaprinter import prettyPrinter -from helpers import download_file, retrieve_url - -# Fix invalid certificate in Windows -import ssl -ssl._create_default_https_context = ssl._create_unverified_context - - -class piratebay(object): - """ Search engine class """ - url = 'https://pirateproxy.live' - name = 'The Pirate Bay' - supported_categories = {'all': '0', 'music': '100', 'movies': '200', - 'games': '400', 'software': '300'} - - def download_torrent(self, info): - """ Downloader """ - print(download_file(info)) - - class MyHtmlParser(HTMLParser): - """ Parser class """ - def __init__(self, results, url): - HTMLParser.__init__(self) - self.results = results - self.url = url - self.current_item = None - self.save_item = None - self.result_table = False # table with results is found - self.result_tbody = False - self.add_query = True - self.result_query = False - - def handle_start_tag_default(self, attrs): - """ Default handler for start tag dispatcher """ - pass - - def handle_start_tag_a(self, attrs): - """ Handler for start tag a """ - params = dict(attrs) - link = params["href"].replace(self.url, '') - if link.startswith("/torrent"): - self.current_item["desc_link"] = "".join((self.url, link)) - self.save_item = "name" - elif link.startswith("magnet"): - self.current_item["link"] = link - # end of the 'name' item - self.current_item['name'] = self.current_item['name'].strip() - self.save_item = None - - def handle_start_tag_font(self, attrs): - """ Handler for start tag font """ - for attr in attrs: - if attr[1] == "detDesc": - self.save_item = "size" - break - - def handle_start_tag_td(self, attrs): - """ Handler for start tag td """ - for attr in attrs: - if attr[1] == "right": - if "seeds" in self.current_item.keys(): - self.save_item = "leech" - else: - self.save_item = "seeds" - break - - def handle_starttag(self, tag, attrs): - """ Parser's start tag handler """ - if self.current_item: - dispatcher = getattr(self, - "_".join(("handle_start_tag", tag)), - self.handle_start_tag_default) - dispatcher(attrs) - - elif self.result_tbody: - if tag == "tr": - self.current_item = {"engine_url": self.url} - - elif tag == "table": - self.result_table = "searchResult" == attrs[0][1] - - elif self.add_query: - if self.result_query and tag == "a": - if len(self.list_searches) < 10: - self.list_searches.append(attrs[0][1]) - else: - self.add_query = False - self.result_query = False - elif tag == "div": - self.result_query = "center" == attrs[0][1] - - def handle_endtag(self, tag): - """ Parser's end tag handler """ - if self.result_tbody: - if tag == "tr": - if 'size' in self.current_item: - # clean up size - temp_data = self.current_item['size'].split() - if "Size" in temp_data: - indx = temp_data.index("Size") - self.current_item['size'] = (temp_data[indx + 1] + " " - + temp_data[indx + 2]) - else: - self.current_item['size'] = -1 - # return result - prettyPrinter(self.current_item) - self.results.append('a') - self.current_item = None - elif tag == "font": - self.save_item = None - elif tag == "table": - self.result_table = self.result_tbody = False - - elif self.result_table: - if tag == "thead": - self.result_tbody = True - elif tag == "table": - self.result_table = self.result_tbody = False - - elif self.add_query and self.result_query: - if tag == "div": - self.add_query = self.result_query = False - - def handle_data(self, data): - """ Parser's data handler """ - if self.save_item: - if (self.save_item == "size" or self.save_item == "name"): - if self.save_item not in self.current_item: - self.current_item[self.save_item] = '' - self.current_item[self.save_item] += " " + data - - else: - self.current_item[self.save_item] = data - self.save_item = None - - def search(self, what, cat='all'): - """ Performs search """ - cat = cat.lower() - # try to get 10 pages (10 * 30 = 300 results) and stop when we don't found results - results_list = [] - parser = self.MyHtmlParser(results_list, self.url) - page = 1 - while page < 11: - # prepare query. 7 is filtering by seeders - page_url = "{0}/search/{1}/{2}/7/{3}".format(self.url, what, page, - self.supported_categories[cat]) - html = retrieve_url(page_url) - parser.feed(html) - if len(results_list) < 1: - break - del results_list[:] - page += 1 - parser.close() diff --git a/nova/engines/rarbg.py b/nova/engines/rarbg.py deleted file mode 100644 index c0af213..0000000 --- a/nova/engines/rarbg.py +++ /dev/null @@ -1,90 +0,0 @@ -#VERSION: 2.12 -# AUTHORS: b0nk -# CONTRIBUTORS: Diego de las Heras (ngosang@hotmail.es) - -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of the author nor the names of its contributors may be -# used to endorse or promote products derived from this software without -# specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -import json -import time -try: - # python3 - from urllib.parse import urlencode, unquote -except ImportError: - # python2 - from urllib import urlencode, unquote - -# qBt -from novaprinter import prettyPrinter -from helpers import retrieve_url - - -class rarbg(object): - url = 'https://rarbg.to' - name = 'RARBG' - supported_categories = { - 'all': '4;14;17;18;23;25;27;28;32;33;40;41;42;44;45;46;47;48;49;50;51;52;53;54', - 'movies': '14;17;42;44;45;46;47;48;50;51;52;54', - 'tv': '18;41;49', - 'music': '23;25', - 'games': '27;28;32;40;53', - 'software': '33' - } - - def search(self, what, cat='all'): - base_url = "https://torrentapi.org/pubapi_v2.php?%s" - app_id = "qbittorrent" - - # get token - params = urlencode({'get_token': 'get_token', 'app_id': app_id}) - response = retrieve_url(base_url % params) - j = json.loads(response) - token = j['token'] - time.sleep(2.1) - - # get response json - what = unquote(what) - category = self.supported_categories[cat] - params = urlencode({'mode': 'search', - 'search_string': what, - 'ranked': 0, - 'category': category, - 'limit': 100, - 'sort': 'seeders', - 'format': 'json_extended', - 'token': token, - 'app_id': 'qbittorrent'}) - response = retrieve_url(base_url % params) - j = json.loads(response) - - # parse results - for result in j['torrent_results']: - res = {'link': result['download'], - 'name': result['title'], - 'size': str(result['size']) + " B", - 'seeds': result['seeders'], - 'leech': result['leechers'], - 'engine_url': self.url, - 'desc_link': result['info_page'] + "&app_id=" + app_id} - prettyPrinter(res) diff --git a/nova/engines/torlock.py b/nova/engines/torlock.py deleted file mode 100644 index d71e17c..0000000 --- a/nova/engines/torlock.py +++ /dev/null @@ -1,101 +0,0 @@ -#VERSION: 2.1 -# AUTHORS: Douman (custparasite@gmx.se) -# CONTRIBUTORS: Diego de las Heras (ngosang@hotmail.es) - -from novaprinter import prettyPrinter -from helpers import retrieve_url, download_file -from re import compile as re_compile -from HTMLParser import HTMLParser - - -class torlock(object): - url = "https://www.torlock.com" - name = "TorLock" - supported_categories = {'all': 'all', - 'anime': 'anime', - 'software': 'software', - 'games': 'game', - 'movies': 'movie', - 'music': 'music', - 'tv': 'television', - 'books': 'ebooks'} - - def download_torrent(self, info): - print(download_file(info)) - - class MyHtmlParser(HTMLParser): - """ Sub-class for parsing results """ - def __init__(self, url): - HTMLParser.__init__(self) - self.url = url - self.article_found = False # true when
with results is found - self.item_found = False - self.item_bad = False # set to True for malicious links - self.current_item = None # dict for found item - self.item_name = None # key's name in current_item dict - self.parser_class = {"ts": "size", - "tul": "seeds", - "tdl": "leech"} - - def handle_starttag(self, tag, attrs): - params = dict(attrs) - if self.item_found: - if tag == "td": - if "class" in params: - self.item_name = self.parser_class.get(params["class"], None) - if self.item_name: - self.current_item[self.item_name] = "" - - elif self.article_found and tag == "a": - if "href" in params: - link = params["href"] - if link.startswith("/torrent"): - self.current_item["desc_link"] = "".join((self.url, link)) - self.current_item["link"] = "".join((self.url, "/tor/", - link.split('/')[2], ".torrent")) - self.current_item["engine_url"] = self.url - self.item_found = True - self.item_name = "name" - self.current_item["name"] = "" - self.item_bad = "rel" in params and params["rel"] == "nofollow" - - elif tag == "article": - self.article_found = True - self.current_item = {} - - def handle_data(self, data): - if self.item_name: - self.current_item[self.item_name] += data - - def handle_endtag(self, tag): - if tag == "article": - self.article_found = False - elif self.item_name and (tag == "a" or tag == "td"): - self.item_name = None - elif self.item_found and tag == "tr": - self.item_found = False - if not self.item_bad: - prettyPrinter(self.current_item) - self.current_item = {} - - def search(self, query, cat='all'): - """ Performs search """ - query = query.replace("%20", "-") - - parser = self.MyHtmlParser(self.url) - page = "".join((self.url, "/", self.supported_categories[cat], - "/torrents/", query, ".html?sort=seeds&page=1")) - html = retrieve_url(page) - parser.feed(html) - - counter = 1 - additional_pages = re_compile(r"/{0}/torrents/{1}.html\?sort=seeds&page=[0-9]+" - .format(self.supported_categories[cat], query)) - list_searches = additional_pages.findall(html)[:-1] # last link is next(i.e. second) - for page in map(lambda link: "".join((self.url, link)), list_searches): - html = retrieve_url(page) - parser.feed(html) - counter += 1 - if counter > 3: - break - parser.close() diff --git a/nova/engines/torrentscsv.py b/nova/engines/torrentscsv.py deleted file mode 100644 index d775b2e..0000000 --- a/nova/engines/torrentscsv.py +++ /dev/null @@ -1,82 +0,0 @@ -#VERSION: 1.0 -# AUTHORS: Dessalines - -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright -# notice, this list of conditions and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of the author nor the names of its contributors may be -# used to endorse or promote products derived from this software without -# specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -import json -try: - # python3 - from urllib.parse import urlencode -except ImportError: - # python2 - from urllib import urlencode - -# qBt -from novaprinter import prettyPrinter -from helpers import retrieve_url - - -class torrentscsv(object): - url = 'https://torrents-csv.ml' - name = 'torrents-csv' - supported_categories = {'all': ''} - - # initialize trackers for magnet links - trackers_list = [ - 'udp://tracker.coppersurfer.tk:6969/announce', - 'udp://tracker.leechers-paradise.org:6969/announce', - 'udp://tracker.opentrackr.org:1337/announce', - 'udp://tracker.openbittorrent.com:80/announce', - 'udp://exodus.desync.com:6969/announce', - 'udp://9.rarbg.me:2710/announce', - 'udp://9.rarbg.to:2710/announce', - 'udp://tracker.tiny-vps.com:6969/announce', - 'udp://retracker.lanta-net.ru:2710/announce', - 'udp://open.demonii.si:1337/announce' - ] - trackers = '&'.join(urlencode({'tr': tracker}) for tracker in trackers_list) - - def search(self, what, cat='all'): - search_url = "{}/service/search?size=300&q={}".format(self.url, what) - desc_url = "{}/#/search/torrent/{}/1".format(self.url, what) - - # get response json - response = retrieve_url(search_url) - response_json = json.loads(response) - - # parse results - for result in response_json: - res = {'link': self.download_link(result), - 'name': result['name'], - 'size': str(result['size_bytes']) + " B", - 'seeds': result['seeders'], - 'leech': result['leechers'], - 'engine_url': self.url, - 'desc_link': desc_url} - prettyPrinter(res) - - def download_link(self, result): - return "magnet:?xt=urn:btih:{}&{}&{}".format( - result['infohash'], urlencode({'dn': result['name']}), self.trackers) diff --git a/nova/engines/versions.txt b/nova/engines/versions.txt deleted file mode 100644 index edf5111..0000000 --- a/nova/engines/versions.txt +++ /dev/null @@ -1,10 +0,0 @@ -eztv: 1.10 -jackett: 3.4 -leetx: 2.2 -legittorrents: 2.4 -limetorrents: 4.5 -piratebay: 2.20 -rarbg: 2.12 -torlock: 2.1 -torrentscsv: 1.0 -zooqle: 1.13 diff --git a/nova/engines/zooqle.py b/nova/engines/zooqle.py deleted file mode 100644 index 39a32ad..0000000 --- a/nova/engines/zooqle.py +++ /dev/null @@ -1,119 +0,0 @@ -#VERSION: 1.13 -# AUTHORS: Kanishk Singh (https://github.com/ArionMiles/) -# CONTRIBUTORS: affaff (https://github.com/affaff) - -# Copyright (c) 2017 Kanishk Singh - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - - -from xml.dom import minidom -from novaprinter import prettyPrinter -from io import StringIO -import gzip - -user_agent = 'Mozilla/5.0 (X11; Linux i686; rv:38.0) Gecko/20100101 Firefox/38.0' -headers = {'User-Agent': user_agent} - -try: - from urllib2 import urlopen, Request, URLError -except ImportError: - from urllib.request import urlopen, Request, URLError - - -def retrieve_url_nodecode(url): - """ Return the content of the url page as a string """ - req = Request(url, headers=headers) - try: - response = urlopen(req) - except URLError as errno: - print(" ".join(("Connection error:", str(errno.reason)))) - print(" ".join(("URL:", url))) - return "" - dat = response.read() - # Check if it is gzipped - if dat[:2] == '\037\213': - # Data is gzip encoded, decode it - compressedstream = StringIO(dat) - gzipper = gzip.GzipFile(fileobj=compressedstream) - extracted_data = gzipper.read() - dat = extracted_data - return dat - return dat - - -class zooqle(object): - """ Search engine class """ - url = 'https://zooqle.com' - name = 'Zooqle' - supported_categories = {'all': 'all', - 'movies': 'Movies', - 'tv': 'TV', - 'music': 'Music', - 'games': 'Games', - 'anime': 'Anime', - 'software': 'Apps', - 'books': 'Books'} - - def search(self, what, cat="all"): - """ Performs search """ - page = 1 - while page < 11: - query = "".join((self.url, "/search?q=", what, - "+category%3A", self.supported_categories[cat], "&fmt=rss")) - if page > 1: - query = query + "&pg=" + str(page) - response = retrieve_url_nodecode(query) - xmldoc = minidom.parseString(response) - itemlist = xmldoc.getElementsByTagName('item') - if len(itemlist) == 0: - return - for item in itemlist: - zooqle_dict = zooqle_dict = {"engine_url": self.url} - zooqle_dict['name'] = (item.getElementsByTagName('title')[0] - .childNodes[0].data) - zooqle_dict["size"] = (item.getElementsByTagName('enclosure')[0] - .attributes['length'].childNodes[0].data) - if zooqle_dict["size"] == '0': - zooqle_dict["link"] = (item.getElementsByTagName('torrent:magnetURI')[0] - .childNodes[0].data) - else: - zooqle_dict["link"] = (item.getElementsByTagName('enclosure')[0] - .attributes['url'].value) - zooqle_dict["desc_link"] = (item.getElementsByTagName('link')[0] - .childNodes[0].data) - zooqle_dict["leech"] = (item.getElementsByTagName('torrent:peers')[0] - .childNodes[0].data) - if not zooqle_dict["leech"].isdigit(): - zooqle_dict["leech"] = '' - zooqle_dict["seeds"] = (item.getElementsByTagName('torrent:seeds')[0] - .childNodes[0].data) - if not zooqle_dict["seeds"].isdigit(): - zooqle_dict["seeds"] = '' - prettyPrinter(zooqle_dict) - totalResultVal = (xmldoc.getElementsByTagName('opensearch:totalResults')[0] - .childNodes[0].data) - startIndex = (xmldoc.getElementsByTagName('opensearch:startIndex')[0] - .childNodes[0].data) - itemsPerPage = (xmldoc.getElementsByTagName('opensearch:itemsPerPage')[0] - .childNodes[0].data) - if (int(startIndex) + int(itemsPerPage)) > int(totalResultVal): - return - page += 1 - return