.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # qbPornolab
2 | qBittorrent Pornolab.net plugin
3 |
4 | Edit pornolab.py by replacing YOUR_USERNAME_HERE and YOUR_PASSWORD_HERE with your Pornolab username and password.
5 |
--------------------------------------------------------------------------------
/pornolab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TainakaDrums/qbPornolab/18bc6e12e39d04e418d5fd8f2e1e9c19206a1b9e/pornolab.png
--------------------------------------------------------------------------------
/pornolab.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | #VERSION: 1.0
4 | #AUTHORS: TainakaDrums [tainakadrums@yandex.ru]
5 | """Pornolab search engine plugin for qBittorrent."""
6 |
7 | # Replace YOUR_USERNAME_HERE and YOUR_PASSWORD_HERE with your Pornolab username and password
8 | credentials = {
9 | 'login_username': '',
10 | 'login_password': '',
11 | }
12 |
13 | # Logging
14 | import logging
15 | logger = logging.getLogger()
16 | # logger.setLevel(logging.DEBUG)
17 | logger.setLevel(logging.WARNING)
18 |
19 | # Try blocks are used to circumvent Python2/3 modules discrepancies and use a single script for both versions.
20 | try:
21 | import cookielib
22 | except ImportError:
23 | import http.cookiejar as cookielib
24 |
25 | try:
26 | from urllib import urlencode, quote, unquote
27 | from urllib2 import build_opener, HTTPCookieProcessor, URLError, HTTPError
28 | except ImportError:
29 | from urllib.parse import urlencode, quote, unquote
30 | from urllib.request import build_opener, HTTPCookieProcessor
31 | from urllib.error import URLError, HTTPError
32 |
33 | try:
34 | from HTMLParser import HTMLParser
35 | except ImportError:
36 | from html.parser import HTMLParser
37 |
38 | import tempfile
39 | import os
40 | import re
41 |
42 | from novaprinter import prettyPrinter
43 |
44 | def dict_encode(dict, encoding='cp1251'):
45 | """Encode dict values to encoding (default: cp1251)."""
46 | encoded_dict = {}
47 | for key in dict:
48 | encoded_dict[key] = dict[key].encode(encoding)
49 | return encoded_dict
50 |
51 | class pornolab(object):
52 | """Pornolab search engine plugin for qBittorrent."""
53 | name = 'Pornolab'
54 | url = 'https://pornolab.net' # We MUST produce an URL attribute at instantiation time, otherwise qBittorrent will fail to register the engine, see #15
55 |
56 | @property
57 | def forum_url(self):
58 | return self.url + '/forum'
59 |
60 | @property
61 | def login_url(self):
62 | return self.forum_url + '/login.php'
63 |
64 | @property
65 | def download_url(self):
66 | return self.forum_url + '/dl.php'
67 |
68 | @property
69 | def search_url(self):
70 | return self.forum_url + '/tracker.php'
71 |
72 | def __init__(self):
73 | """Initialize Pornolab search engine, signing in using given credentials."""
74 | # Initialize various objects.
75 | self.cj = cookielib.CookieJar()
76 | self.opener = build_opener(HTTPCookieProcessor(self.cj))
77 | self.url = 'https://pornolab.net' # Override url with the actual URL to be used (in case official URL isn't accessible)
78 | self.credentials = credentials
79 | # Add submit button additional POST param.
80 | self.credentials['login'] = u'Вход'
81 | try:
82 | logging.info("Trying to connect using given credentials.")
83 | response = self.opener.open(self.login_url, urlencode(dict_encode(self.credentials)).encode())
84 | # Check if response status is OK.
85 | if response.getcode() != 200:
86 | raise HTTPError(response.geturl(), response.getcode(), "HTTP request to {} failed with status: {}".format(self.login_url, response.getcode()), response.info(), None)
87 | # Check if login was successful using cookies.
88 | if not 'bb_data' in [cookie.name for cookie in self.cj]:
89 | logging.debug(self.cj)
90 | raise ValueError("Unable to connect using given credentials.")
91 | else:
92 | logging.info("Login successful.")
93 | except (URLError, HTTPError, ValueError) as e:
94 | logging.error(e)
95 |
96 | def download_torrent(self, url):
97 | """Download file at url and write it to a file, print the path to the file and the url."""
98 | # Make temp file.
99 | file, path = tempfile.mkstemp('.torrent')
100 | file = os.fdopen(file, "wb")
101 | # Set up fake POST params, needed to trick the server into sending the file.
102 | id = re.search(r'dl\.php\?t=(\d+)', url).group(1)
103 | post_params = {'t': id,}
104 | # Download torrent file at url.
105 | try:
106 | response = self.opener.open(url, urlencode(dict_encode(post_params)).encode())
107 | # Only continue if response status is OK.
108 | if response.getcode() != 200:
109 | raise HTTPError(response.geturl(), response.getcode(), "HTTP request to {} failed with status: {}".format(url, response.getcode()), response.info(), None)
110 | except (URLError, HTTPError) as e:
111 | logging.error(e)
112 | raise e
113 | # Write it to a file.
114 | data = response.read()
115 | file.write(data)
116 | file.close()
117 | # Print file path and url.
118 | print(path+" "+url)
119 |
120 | class Parser(HTMLParser):
121 | """Implement a simple HTML parser to parse results pages."""
122 |
123 | def __init__(self, engine):
124 | """Initialize the parser with url and tell him if he's on the first page of results or not."""
125 |
126 | HTMLParser.__init__(self, convert_charrefs=True)
127 |
128 | self.engine = engine
129 | self.results = []
130 | self.other_pages = []
131 | self.cat_re = re.compile(r'tracker\.php\?f=\d+')
132 | self.pages_re = re.compile(r'tracker\.php\?.*?start=(\d+)')
133 | self.reset_current()
134 |
135 | def reset_current(self):
136 | """Reset current_item (i.e. torrent) to default values."""
137 | self.current_item = {'cat': None,
138 | 'name': None,
139 | 'link': None,
140 | 'size': None,
141 | 'seeds': None,
142 | 'leech': None,
143 | 'desc_link': None,}
144 |
145 | def handle_data(self, data):
146 | """Retrieve inner text information based on rules defined in do_tag()."""
147 | for key in self.current_item:
148 | if self.current_item[key] == True:
149 | if key == 'size':
150 | self.current_item['size'] = data.replace('\xa0', '')
151 | else:
152 | self.current_item[key] = data
153 |
154 | def handle_starttag(self, tag, attrs):
155 | """Pass along tag and attributes to dedicated handlers. Discard any tag without handler."""
156 | try:
157 | getattr(self, 'do_{}'.format(tag))(attrs)
158 | except:
159 | pass
160 |
161 | def handle_endtag(self, tag):
162 | """Add last item manually on html end tag."""
163 | # We add last item found manually because items are added on new
164 | # and not on
(can't do it without the attribute).
165 | if tag == 'html' and self.current_item['seeds']:
166 | self.results.append(self.current_item)
167 |
168 | def do_tr(self, attr):
169 | """ is the big container for one torrent, so we store current_item and reset it."""
170 | params = dict(attr)
171 | if 'tCenter' in params.get('class', ''):
172 |
173 | if self.current_item['seeds']:
174 | self.results.append(self.current_item)
175 | self.reset_current()
176 |
177 | def do_a(self, attr):
178 | """ tags can specify torrent link in "href" or category or name or size in inner text. Also used to retrieve further results pages."""
179 | params = dict(attr)
180 | try:
181 | if self.cat_re.search(params['href']):
182 | self.current_item['cat'] = True
183 | elif 'tLink' in params['class'] and not self.current_item['desc_link']:
184 | self.current_item['desc_link'] = self.engine.forum_url + params['href'][1:]
185 | self.current_item['link'] = self.engine.download_url + params['href'].split('viewtopic.php')[-1]
186 | self.current_item['name'] = True
187 | elif self.current_item['size'] == None and 'dl-stub' in params['class']:
188 | self.current_item['size'] = True
189 | # If we're on the first page of results, we search for other pages.
190 | elif self.first_page:
191 | pages = self.pages_re.search(params['href'])
192 | if pages:
193 | if pages.group(1) not in self.other_pages:
194 | self.other_pages.append(pages.group(1))
195 | except KeyError:
196 | pass
197 |
198 | def do_td(self, attr):
199 | """ tags give us number of leechers in inner text and can signal torrent size in next tag."""
200 | params = dict(attr)
201 | try:
202 | if 'leechmed' in params['class']:
203 | self.current_item['leech'] = True
204 | except KeyError:
205 | pass
206 |
207 | def do_b(self, attr):
208 | """ give us number of seeders in inner text."""
209 | params = dict(attr)
210 | if 'seedmed' in params.get('class', ''):
211 | self.current_item['seeds'] = True
212 |
213 | def search(self, what, start=0):
214 | """Search for what starting on specified page. Defaults to first page of results."""
215 | logging.debug("parse_search({}, {})".format(what, start))
216 |
217 | # If we're on first page of results, we'll try to find other pages
218 | if start == 0:
219 | self.first_page = True
220 | else:
221 | self.first_page = False
222 |
223 | try:
224 | response = self.engine.opener.open('{}?nm={}&start={}'.format(self.engine.search_url, quote(what), start))
225 | # Only continue if response status is OK.
226 | if response.getcode() != 200:
227 | raise HTTPError(response.geturl(), response.getcode(), "HTTP request to {} failed with status: {}".format(self.engine.search_url, response.getcode()), response.info(), None)
228 | except (URLError, HTTPError) as e:
229 | logging.error(e)
230 | raise e
231 |
232 | # Decode data and feed it to parser
233 | data = response.read().decode('cp1251')
234 | data = re.sub(r'||<\/b>', '', data)
235 | self.feed(data)
236 |
237 | def search(self, what, cat='all'):
238 | """Search for what on the search engine."""
239 | # Instantiate parser
240 | self.parser = self.Parser(self)
241 |
242 | # Decode search string
243 | what = unquote(what)
244 | logging.info("Searching for {}...".format(what))
245 |
246 | # Search on first page.
247 | logging.info("Parsing page 1.")
248 | self.parser.search(what)
249 |
250 | # If multiple pages of results have been found, repeat search for each page.
251 | logging.info("{} pages of results found.".format(len(self.parser.other_pages)+1))
252 | for start in self.parser.other_pages:
253 | logging.info("Parsing page {}.".format(int(start)//50+1))
254 | self.parser.search(what, start)
255 |
256 | # PrettyPrint each torrent found, ordered by most seeds
257 | self.parser.results.sort(key=lambda torrent:torrent['seeds'], reverse=True)
258 | for torrent in self.parser.results:
259 |
260 | torrent['engine_url'] = 'https://pornolab.net' # Kludge, see #15
261 | if __name__ != "__main__": # This is just to avoid printing when I debug.
262 | prettyPrinter(torrent)
263 | else:
264 | print(torrent)
265 |
266 |
267 | self.parser.close()
268 | logging.info("{} torrents found.".format(len(self.parser.results)))
269 |
270 | # For testing purposes.
271 | if __name__ == "__main__":
272 | engine = pornolab()
273 | # engine.search('2020')
274 |
--------------------------------------------------------------------------------
|