├── .gitignore
├── README.md
├── README.ru.md
├── darklibria.png
├── darklibria.py
├── lostfilm.png
└── lostfilm.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.json
2 | *.log
3 | *.bat
4 |
5 | helpers.py
6 | novaprinter.py
7 | socks.py
8 |
9 | __pycache__
10 | .vscode
11 | install
12 | proxycheck
13 | venv
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # qBittorrent search plugins
2 |
3 | [Readme in russian](https://github.com/bugsbringer/qbit-plugins/blob/master/README.ru.md)
4 |
5 | Plugins
6 | -------
7 |
8 | * LostFilm.TV
9 | * dark-libria.it
10 |
11 | Installation
12 | ------------
13 |
14 | 1.For private torrent-trackers(LostFilm):
15 |
16 | * Save *.py file in any convenient directory
17 | * Open this file using **notepad** or any other **text editor**
18 | * Replace text in rows **EMAIL** and **PASSWORD** with **your login data**
19 | Example:
20 |
21 | EMAIL = "example123@gmail.com"
22 | PASSWORD = "qwerty345"
23 |
24 | 2.Follow the **official tutorial**: [Install search plugins](https://github.com/qbittorrent/search-plugins/wiki/Install-search-plugins)
25 |
26 | LostFilm plugin features
27 | ------------------------
28 |
29 | * Information about seeders and leechers in search results. (Reduces search speed)
30 | You can disable this functionality in lostfilm.py file:
31 |
32 | ENABLE_PEERS_INFO = False
33 |
34 | * Additional search queries:
35 | *just enter it in search field*
36 | * Favorites serials:
37 |
38 | @fav
39 |
40 | * New episodes in the last 7 days:
41 |
42 | @new
43 |
44 | * Among favorites:
45 |
46 | @new:fav
47 |
48 | * New in version 0.21
49 | Now you can change site root url, to solve the blocking issies:
50 |
51 |
52 | SITE_URL = "https://www.lostfilm.tv"
53 |
54 | Errors
55 | ------
56 |
57 | ### Captcha requested
58 |
59 | * You need to **logout**:
60 | * Go to
61 | * Then aprove logout
62 | * **Login** again:
63 | * Go to
64 | * Enter your login data and captcha
65 | * And finally Log in
66 |
67 | ### Connection failed
68 |
69 | * Could not connect to server
70 |
71 | ### Incorrect login data
72 |
73 | * Most likely you incorrectly filled in the authorization data
74 |
--------------------------------------------------------------------------------
/README.ru.md:
--------------------------------------------------------------------------------
1 | # qBittorrent плагины
2 |
3 | Плагины
4 | -------
5 |
6 | * LostFilm.TV
7 | * dark-libria.it
8 |
9 | Установка
10 | ---------
11 |
12 | 1.Для приватных торрент-трекеров(LostFilm):
13 |
14 | * Сохраните файл *.py в любую удобную директорию
15 | * Откройте файл при помощи **блокнота** или другого **текстового редактора**
16 | * Замените текст в строках **EMAIL** и **PASSWORD** **своими данными для входа на сайт**
17 | Пример:
18 |
19 | EMAIL = "example123@gmail.com"
20 | PASSWORD = "qwerty345"
21 |
22 | 2.Следуйте **официальному руководству**: [Install search plugins](https://github.com/qbittorrent/search-plugins/wiki/Install-search-plugins)
23 |
24 | Особенности плагина LostFilm
25 | ----------------------------
26 |
27 | * Информация о сидах и личах в результатах поиска. (Замедляет поиск)
28 | Вы можете отключить эту функцию в lostfilm.py файле:
29 |
30 | ENABLE_PEERS_INFO = False
31 |
32 | * Дополнительные поисковые запросы:
33 | *Просто введите в поле поиска*
34 | * Избранные сериалы:
35 |
36 | @fav
37 |
38 | * Новые эпизоды:
39 | * Среди всех или среди избранных(за последние 7 дней):
40 |
41 | @new
42 | @new:fav
43 |
44 | New in version 0.21
45 | * Теперь вы можете изменить корневой адрес сайта, чтобы решить проблемы с блокировкой:
46 |
47 |
48 | SITE_URL = "https://www.lostfilm.tv"
49 |
50 | Ошибки
51 | ------
52 |
53 | ### Captcha requested
54 |
55 | * Вам нужно **выйти** из своего аккаунта на сайте:
56 | * Перейдите по ссылке
57 | * Подтвердите выход
58 | * **Войти** опять:
59 | * Перейдите по ссылке
60 | * Введи ваши данные для входа и капчу
61 | * Войдите :)
62 |
63 | ### Connection failed
64 |
65 | * Не удалось подключиться к серверу
66 |
67 | ### Incorrect login data
68 |
69 | * Похоже что поля **EMAIL** и **PASSWORD** заполнены не верно
70 |
--------------------------------------------------------------------------------
/darklibria.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bugsbringer/qbit-plugins/0ee6392ded7953cb50ac913a29dd839b1e53bdda/darklibria.png
--------------------------------------------------------------------------------
/darklibria.py:
--------------------------------------------------------------------------------
1 | #VERSION: 0.13
2 | #AUTHORS: Bugsbringer (dastins193@gmail.com)
3 |
4 |
5 | SITE_URL = 'https://darklibria.it/'
6 |
7 |
8 | import logging
9 | import os
10 | from concurrent.futures import ThreadPoolExecutor
11 | from datetime import datetime
12 | from html.parser import HTMLParser
13 | from math import ceil
14 | from re import compile as re_compile
15 | from time import mktime
16 | from urllib import parse
17 |
18 | from helpers import retrieve_url
19 | from novaprinter import prettyPrinter
20 |
21 | LOG_FORMAT = '[%(asctime)s] %(levelname)s:%(name)s:%(funcName)s - %(message)s'
22 | LOG_DT_FORMAT = '%d-%b-%y %H:%M:%S'
23 |
24 |
25 | class darklibria:
26 | url = SITE_URL
27 | name = 'dark-libria'
28 | supported_categories = {'all': '0'}
29 |
30 | units_dict = {"Тб": "TB", "Гб": "GB", "Мб": "MB", "Кб": "KB", "б": "B"}
31 | page_search_url_pattern = SITE_URL + 'search?page={page}&find={what}'
32 | dt_regex = re_compile('\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')
33 |
34 | def __init__(self, output=True):
35 | self.output = output
36 |
37 | def search(self, what, cat='all'):
38 | self.torrents_count = 0
39 | what = parse.quote(parse.unquote(what))
40 | logger.info(parse.unquote(what))
41 | self.set_search_data(self.handle_page(what, 1))
42 | with ThreadPoolExecutor() as executor:
43 | for page in range(2, self.pages_count + 1):
44 | executor.submit(self.handle_page, what, page)
45 | logger.info('%s torrents', self.torrents_count)
46 |
47 | def handle_page(self, what, page):
48 | url = self.page_search_url_pattern.format(page=page, what=what)
49 | data = self.request_get(url)
50 | if not data:
51 | return
52 | parser = Parser(data)
53 | serials = parser.find_all('tbody', {'style': 'vertical-align: center'})
54 | with ThreadPoolExecutor() as executor:
55 | for serial in serials:
56 | executor.submit(self.handle_serial, serial.a['href'])
57 | return parser
58 |
59 | def handle_serial(self, url):
60 | data = self.request_get(url)
61 | if not data:
62 | return
63 | parser = Parser(data)
64 | name = parser.find(attrs={'id': 'russian_name'}).text
65 | for torrent_row in parser.find_all('tr', {'class': 'torrent'}):
66 | self.handle_torrent_row(torrent_row, name, url)
67 |
68 | def handle_torrent_row(self, torrent_row, name, url):
69 | type, quality, size_data, date_time, download, seeds, leech, *_ = torrent_row.children
70 | self.pretty_printer({
71 | 'link': self.get_link(download),
72 | 'name': self.get_name(name, quality, type, date_time),
73 | 'size': self.get_size(size_data),
74 | 'seeds': int(seeds.text),
75 | 'leech': int(leech.text),
76 | 'engine_url': self.url,
77 | 'desc_link': url
78 | })
79 | self.torrents_count += 1
80 |
81 | def get_link(self, download):
82 | return download.find(attrs={'title': 'Magnet-ссылка'})['href'] \
83 | or download.find(attrs={'title': 'Скачать торрент'})['href']
84 |
85 | def get_name(self, name, quality, type, date_time):
86 | return '[{}] {} [{}] {}'.format(
87 | self.get_date(date_time),
88 | name,
89 | type.text,
90 | quality.text
91 | )
92 |
93 | def get_date(self, date_time):
94 | utc_dt_string = self.dt_regex.search(date_time.text).group()
95 | utc = datetime.strptime(utc_dt_string, '%Y-%m-%d %H:%M:%S')
96 | return str(utc2local(utc))
97 |
98 | def get_size(self, size_data):
99 | size, unit = size_data.text.split()
100 | return size + ' ' + self.units_dict[unit]
101 |
102 | def request_get(self, url):
103 | try:
104 | return retrieve_url(url)
105 | except Exception as exp:
106 | logger.error(exp)
107 | self.pretty_printer({
108 | 'link': 'Error',
109 | 'name': 'Connection failed',
110 | 'size': "0",
111 | 'seeds': -1,
112 | 'leech': -1,
113 | 'engine_url': self.url,
114 | 'desc_link': self.url
115 | })
116 |
117 | def pretty_printer(self, dictionary):
118 | logger.debug(str(dictionary))
119 | if self.output:
120 | prettyPrinter(dictionary)
121 |
122 | def set_search_data(self, parser):
123 | results = parser.find('span', {'class': 'text text-light mt-0'})
124 | if results:
125 | parts = results.text.split()
126 | items_count = int(parts[4])
127 | items_on_page = int(parts[2].split('-')[1])
128 | self.pages_count = ceil(items_count / items_on_page)
129 |
130 | logger.info('%s animes', items_count)
131 | else:
132 | self.pages_count = 0
133 |
134 | logger.info('%s pages', self.pages_count)
135 |
136 |
137 | class Tag:
138 | def __init__(self, tag=None, attrs=(), is_self_closing=None):
139 | self.type = tag
140 | self.is_self_closing = is_self_closing
141 | self._attrs = tuple(attrs)
142 | self._content = tuple()
143 |
144 | @property
145 | def attrs(self):
146 | """returns dict of Tag's attrs"""
147 | return dict(self._attrs)
148 |
149 | @property
150 | def text(self):
151 | """returns str of all contained text"""
152 | return ''.join(c if isinstance(c, str) else c.text for c in self._content)
153 |
154 | def _add_content(self, obj):
155 | if isinstance(obj, (Tag, str)):
156 | self._content += (obj,)
157 | else:
158 | raise TypeError('Argument must be str or %s, not %s' %
159 | (self.__class__, obj.__class__))
160 |
161 | def find(self, tag=None, attrs=None):
162 | """returns Tag or None"""
163 | return next(self._find_all(tag, attrs), None)
164 |
165 | def find_all(self, tag=None, attrs=None):
166 | """returns list"""
167 | return list(self._find_all(tag, attrs))
168 |
169 | def _find_all(self, tag_type=None, attrs=None):
170 | """returns generator"""
171 | if not (isinstance(tag_type, (str, Tag)) or tag_type is None):
172 | raise TypeError(
173 | 'tag_type argument must be str or Tag, not %s' % (tag_type.__class__))
174 |
175 | if not (isinstance(attrs, dict) or attrs is None):
176 | raise TypeError('attrs argument must be dict, not %s' %
177 | (self.__class__))
178 |
179 | # get tags-descendants generator
180 | results = self.descendants
181 |
182 | # filter by Tag.type
183 | if tag_type:
184 | if isinstance(tag_type, Tag):
185 | tag_type, attrs = tag_type.type, (
186 | attrs if attrs else tag_type.attrs)
187 |
188 | results = filter(lambda t: t.type == tag_type, results)
189 |
190 | # filter by Tag.attrs
191 | if attrs:
192 | # remove Tags without attrs
193 | results = filter(lambda t: t._attrs, results)
194 |
195 | def filter_func(tag):
196 | for key in attrs.keys():
197 | if attrs[key] not in tag.attrs.get(key, ()):
198 | return False
199 | return True
200 |
201 | # filter by attrs
202 | results = filter(filter_func, results)
203 |
204 | yield from results
205 |
206 | @property
207 | def children(self):
208 | """returns generator of tags-children"""
209 | return (obj for obj in self._content if isinstance(obj, Tag))
210 |
211 | @property
212 | def descendants(self):
213 | """returns generator of tags-descendants"""
214 | for child_tag in self.children:
215 | yield child_tag
216 | yield from child_tag.descendants
217 |
218 | def __getitem__(self, key):
219 | return self.attrs[key]
220 |
221 | def __getattr__(self, attr):
222 | if not attr.startswith("__"):
223 | return self.find(tag=attr)
224 |
225 | def __repr__(self):
226 | attrs = ' '.join(str(k) if v is None else '{}="{}"'.format(k, v)
227 | for k, v in self._attrs)
228 | starttag = ' '.join((self.type, attrs)) if attrs else self.type
229 |
230 | if self.is_self_closing:
231 | return '<{starttag}>\n'.format(starttag=starttag)
232 | else:
233 | nested = '\n' * bool(next(self.children, None)) + \
234 | ''.join(map(str, self._content))
235 | return '<{}>{}{}>\n'.format(starttag, nested, self.type)
236 |
237 |
238 | class Parser(HTMLParser):
239 | def __init__(self, html_code, *args, **kwargs):
240 | super().__init__(*args, **kwargs)
241 |
242 | self._root = Tag('_root')
243 | self._path = [self._root]
244 |
245 | self.feed(''.join(map(str.strip, html_code.splitlines())))
246 | self.handle_endtag(self._root.type)
247 | self.close()
248 |
249 | self.find = self._root.find
250 | self.find_all = self._root.find_all
251 |
252 | @property
253 | def attrs(self):
254 | return self._root.attrs
255 |
256 | @property
257 | def text(self):
258 | return self._root.text
259 |
260 | def handle_starttag(self, tag, attrs):
261 | self._path.append(Tag(tag=tag, attrs=attrs))
262 |
263 | def handle_endtag(self, tag_type):
264 | for pos, tag in tuple(enumerate(self._path))[::-1]:
265 | if isinstance(tag, Tag) and tag.type == tag_type and tag.is_self_closing is None:
266 | tag.is_self_closing = False
267 |
268 | for obj in self._path[pos + 1:]:
269 | if isinstance(obj, Tag) and obj.is_self_closing is None:
270 | obj.is_self_closing = True
271 |
272 | tag._add_content(obj)
273 |
274 | self._path = self._path[:pos + 1]
275 |
276 | break
277 |
278 | def handle_startendtag(self, tag, attrs):
279 | self._path.append(Tag(tag=tag, attrs=attrs, is_self_closing=True))
280 |
281 | def handle_decl(self, decl):
282 | self._path.append(Tag(tag='!'+decl, is_self_closing=True))
283 |
284 | def handle_data(self, text):
285 | self._path.append(text)
286 |
287 | def __getitem__(self, key):
288 | return self.attrs[key]
289 |
290 | def __getattr__(self, attr):
291 | if not attr.startswith("__"):
292 | return getattr(self._root, attr)
293 |
294 | def __repr__(self):
295 | return ''.join(str(c) for c in self._root._content)
296 |
297 |
298 | def utc2local(utc):
299 | epoch = mktime(utc.timetuple())
300 | offset = datetime.fromtimestamp(epoch) - datetime.utcfromtimestamp(epoch)
301 | return utc + offset
302 |
303 |
304 | is_main = __name__ == '__main__'
305 | STORAGE = os.path.abspath(os.path.dirname(__file__))
306 | log_config = {
307 | 'level': logging.INFO if is_main else logging.WARNING,
308 | 'filename': None if is_main else os.path.join(STORAGE, 'darklibria.log'),
309 | 'format': LOG_FORMAT,
310 | 'datefmt': LOG_DT_FORMAT
311 | }
312 | logging.basicConfig(**log_config)
313 | logger = logging.getLogger('darklibria')
314 |
315 | if is_main:
316 | import sys
317 | darklibria(output=False).search(sys.argv[-1])
318 |
--------------------------------------------------------------------------------
/lostfilm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bugsbringer/qbit-plugins/0ee6392ded7953cb50ac913a29dd839b1e53bdda/lostfilm.png
--------------------------------------------------------------------------------
/lostfilm.py:
--------------------------------------------------------------------------------
1 | #VERSION: 0.22
2 | #AUTHORS: Bugsbringer (dastins193@gmail.com)
3 |
4 |
5 | EMAIL = "YOUR_EMAIL"
6 | PASSWORD = "YOUR_PASSWORD"
7 | ENABLE_PEERS_INFO = True
8 | SITE_URL = "https://www.lostfilm.tv"
9 |
10 | proxy = {
11 | 'enable': False,
12 |
13 | 'proxy_urls': {
14 | 'http': 'ip:port',
15 | 'https': 'ip:port'
16 | },
17 |
18 | 'auth': False,
19 | 'username': '',
20 | 'password': ''
21 | }
22 |
23 | import concurrent.futures
24 | import hashlib
25 | import json
26 | import logging
27 | import os
28 | import re
29 | from collections import OrderedDict
30 | from datetime import datetime
31 | from html.parser import HTMLParser
32 | from http.cookiejar import CookieJar
33 | from io import BytesIO
34 | from time import time
35 | from urllib import parse, request
36 |
37 | from novaprinter import prettyPrinter
38 |
39 | STORAGE = os.path.abspath(os.path.dirname(__file__))
40 | is_main = __name__ == '__main__'
41 |
42 | # logging
43 | log_config = {
44 | 'level': 'DEBUG' if is_main else 'ERROR',
45 | 'format': '[%(asctime)s] %(levelname)s:%(name)s:%(funcName)s - %(message)s',
46 | 'datefmt': '%d-%b-%y %H:%M:%S'
47 | }
48 |
49 | if not is_main:
50 | log_config.update({'filename': os.path.join(STORAGE, 'lostfilm.log')})
51 |
52 | logging.basicConfig(**log_config)
53 | logger = logging.getLogger('lostfilm')
54 | logger.setLevel(logging.WARNING)
55 |
56 |
57 | class lostfilm:
58 | url = SITE_URL
59 | name = 'LostFilm'
60 | supported_categories = {'all': '0'}
61 |
62 | search_url_pattern = SITE_URL + '/search/?q={what}'
63 | serial_url_pattern = SITE_URL + '{href}/seasons'
64 | download_url_pattern = SITE_URL + '/v_search.php?a={code}'
65 | season_url_pattern = SITE_URL + '{href}/season_{season}'
66 | episode_url_pattern = SITE_URL + '{href}/season_{season}/episode_{episode}/'
67 | additional_url_pattern = SITE_URL + '{href}/additional/episode_{episode}/'
68 | new_url_pattern = SITE_URL + '/new/page_{page}/type_{type}'
69 |
70 | additional_season = 999
71 | all_episodes = 999
72 | peer_id = '-PC0001-' + str(time()).replace('.', '')[-12:]
73 |
74 | datetime_format = '%d.%m.%Y'
75 | units_dict = {"ТБ": "TB", "ГБ": "GB", "МБ": "MB", "КБ": "KB", "Б": "B"}
76 |
77 | def __init__(self, output=True):
78 | self.output = output
79 | self.session = Session()
80 |
81 | def search(self, what, cat='all'):
82 | logger.info(what)
83 |
84 | self.torrents_count = 0
85 | self.prevs = set()
86 | self.old_seasons = dict()
87 |
88 | if not self.session.is_actual:
89 | self.pretty_printer({
90 | 'link': 'Error',
91 | 'name': self.session.error,
92 | 'size': "0",
93 | 'seeds': -1,
94 | 'leech': -1,
95 | 'engine_url': self.url,
96 | 'desc_link': self.url
97 | })
98 |
99 | return False
100 |
101 | if parse.unquote(what).startswith('@'):
102 | params = parse.unquote(what)[1:].split(':')
103 |
104 | if params:
105 | if params[0] == 'fav':
106 | self.get_fav()
107 |
108 | elif params[0] == 'new':
109 | if len(params) == 1:
110 | self.get_new()
111 |
112 | elif len(params) == 2 and params[1] == 'fav':
113 | self.get_new(fav=True)
114 | else:
115 | try:
116 | url = self.search_url_pattern.format(what=request.quote(what))
117 | search_result = self.session.request(url)
118 | except Exception as exp:
119 | logger.error(exp)
120 |
121 | else:
122 | serials_tags = Parser(search_result).find_all('div', {'class': 'row-search'})
123 | if serials_tags:
124 | with concurrent.futures.ThreadPoolExecutor() as executor:
125 | for serial_href in (serial.a['href'] for serial in serials_tags):
126 | logger.debug(serial_href)
127 |
128 | executor.submit(self.get_episodes, serial_href)
129 |
130 | logger.info('%s torrents', self.torrents_count)
131 |
132 | def get_new(self, fav=False, days=7):
133 | type = 99 if fav else 0
134 | today = datetime.now().date()
135 | self.dates = {}
136 |
137 | with concurrent.futures.ThreadPoolExecutor() as executor:
138 | page_number = 1
139 | while True:
140 | url = self.new_url_pattern.format(page=page_number, type=type)
141 | page = self.session.request(url)
142 |
143 | rows = Parser(page).find_all('div', {'class': 'row'})
144 |
145 | if not rows:
146 | break
147 |
148 | for row in rows:
149 |
150 | release_date_str = row.find_all('div', {'class': 'alpha'})[1].text
151 | release_date_str = re.search(r'\d{2}.\d{2}.\d{4}', release_date_str)[0]
152 | release_date = datetime.strptime(release_date_str, self.datetime_format).date()
153 |
154 | date_delta = today - release_date
155 |
156 | if date_delta.days > days:
157 | return
158 |
159 | href = '/'.join(row.a['href'].split('/')[:3])
160 |
161 | haveseen_btn = row.find('div', {'onclick': 'markEpisodeAsWatched(this);'})
162 | episode_code = haveseen_btn['data-episode'].rjust(9, '0')
163 |
164 | self.dates[episode_code] = release_date_str
165 |
166 | executor.submit(self.get_torrents, href, episode_code, True)
167 |
168 | page_number += 1
169 |
170 | def get_fav(self):
171 | page = self.session.request(SITE_URL + '/my/type_1')
172 |
173 | with concurrent.futures.ThreadPoolExecutor() as executor:
174 | for serial in Parser(page).find_all('div', {'class': 'serial-box'}):
175 | href = serial.find('a', {'class': 'body'})['href']
176 | executor.submit(self.get_episodes, href)
177 |
178 | def get_episodes(self, serial_href):
179 | self.old_seasons.setdefault(serial_href, 0)
180 |
181 | serial_page = self.session.request(self.serial_url_pattern.format(href=serial_href))
182 | with concurrent.futures.ThreadPoolExecutor() as executor:
183 | for button in Parser(serial_page).find_all('div', {'class': 'external-btn'}):
184 | item_button = button.attrs.get('onclick')
185 |
186 | if item_button:
187 | episode_code = re.search(r'\d{7,9}', item_button)[0].rjust(9, '0')
188 | logger.debug('episode_code = %s', episode_code)
189 | executor.submit(self.get_torrents, serial_href, episode_code)
190 |
191 | def get_torrents(self, href, code, new_episodes=False):
192 | season, episode = int(code[3:6]), int(code[6:])
193 |
194 | if not any((
195 | season > self.old_seasons.get(href, -1),
196 | episode == self.all_episodes,
197 | season == self.additional_season,
198 | new_episodes
199 | )):
200 | return
201 |
202 | redir_page = self.session.request(self.download_url_pattern.format(code=code))
203 | torrent_page_url = re.search(r'(?<=location.replace\(").+(?="\);)', redir_page)
204 |
205 | if not torrent_page_url:
206 | return
207 |
208 | torrent_page = self.session.request(torrent_page_url[0])
209 | date = '' if not new_episodes else '[' + self.dates.pop(code, '') + ']'
210 | desc_link = self.get_description_url(href, code)
211 |
212 | logger.debug('desc_link = %s', desc_link)
213 |
214 | with concurrent.futures.ThreadPoolExecutor() as executor:
215 | for torrent_tag in Parser(torrent_page).find_all('div', {'class': 'inner-box--item'}):
216 | main = torrent_tag.find('div', {'class': 'inner-box--link main'}).a
217 | link, name = main['href'], ' '.join((main.text.replace('\n', ' '), date))
218 |
219 | if not new_episodes:
220 | if link in self.prevs:
221 | self.old_seasons[href] = max(self.old_seasons.get(href, 0), season)
222 | break
223 |
224 | self.prevs.add(link)
225 |
226 | size, unit = re.search(
227 | r'\d+.\d+ \w\w(?=\.)',
228 | torrent_tag.find('div', {'class': 'inner-box--desc'}).text
229 | )[0].split()
230 |
231 | torrent_dict = {
232 | 'link': link,
233 | 'name': name,
234 | 'size': ' '.join((size, self.units_dict.get(unit, ''))),
235 | 'seeds': -1,
236 | 'leech': -1,
237 | 'engine_url': self.url,
238 | 'desc_link': desc_link
239 | }
240 |
241 | if ENABLE_PEERS_INFO:
242 | future = executor.submit(self.get_torrent_info, torrent_dict)
243 | future.add_done_callback(lambda f: self.pretty_printer(f.result()))
244 | else:
245 | self.pretty_printer(torrent_dict)
246 |
247 | def get_description_url(self, href, code):
248 | season, episode = int(code[3:6]), int(code[6:])
249 |
250 | if season == self.additional_season:
251 | return self.additional_url_pattern.format(href=href, episode=episode)
252 |
253 | elif episode == self.all_episodes:
254 | return self.season_url_pattern.format(href=href, season=season)
255 |
256 | else:
257 | return self.episode_url_pattern.format(href=href, season=season, episode=episode)
258 |
259 | def get_torrent_info(self, tdict):
260 | response = self.session.request(tdict['link'], decode=False)
261 | if not response:
262 | return tdict
263 |
264 | torrent = self.decode_data(response)
265 | torrent_info = self.encode_obj(torrent.get(b'info'))
266 | if not torrent_info:
267 | return tdict
268 |
269 | info_hash = hashlib.sha1(torrent_info).digest()
270 |
271 | params = {
272 | 'peer_id': self.peer_id,
273 | 'info_hash': info_hash,
274 | 'port': 6881,
275 | 'left': 0,
276 | 'downloaded': 0,
277 | 'uploaded': 0,
278 | 'compact': 1
279 | }
280 | url = torrent[b'announce'].decode('utf-8') + '?' + parse.urlencode(params)
281 | response = self.session.request(url, decode=False)
282 | if response:
283 | data = self.decode_data(response)
284 | tdict['seeds'] = data.get(b'complete', 0) - 1
285 | tdict['leech'] = data.get(b'incomplete', -1)
286 |
287 | return tdict
288 |
289 | def decode_data(self, data):
290 | try:
291 | return bdecode(data)
292 | except Exception as e:
293 | logger.error(e)
294 | return dict()
295 |
296 | def encode_obj(self, obj):
297 | try:
298 | return bencode(obj)
299 | except Exception as e:
300 | logger.error(e)
301 | return b''
302 |
303 | def pretty_printer(self, dictionary):
304 | if dictionary['link'] == 'Error':
305 | logger.error(dictionary)
306 | else:
307 | logger.debug(dictionary)
308 | self.torrents_count += 1
309 |
310 | if self.output:
311 | try:
312 | prettyPrinter(dictionary.copy())
313 | except OSError as e:
314 | logger.error('error %s on printing %s', e, dictionary)
315 |
316 |
317 | class Session:
318 | site_name = 'lostfilm'
319 | file_name = 'lostfilm.json'
320 | datetime_format = '%m-%d-%y %H:%M:%S'
321 |
322 | token = None
323 | time = None
324 | _error = None
325 |
326 | @property
327 | def error(self):
328 | return 'Error: {info}.'.format(info=self._error)
329 |
330 | @property
331 | def file_path(self):
332 | """path to file with session data"""
333 | return os.path.join(STORAGE, self.file_name)
334 |
335 | @property
336 | def is_actual(self):
337 | """Checks the relevance of the token"""
338 |
339 | if self.token and self.time and not self._error:
340 | delta = datetime.now() - self.time
341 | return delta.days < 1
342 |
343 | return False
344 |
345 | @property
346 | def cookies(self):
347 | if not self.is_actual:
348 | self.create_new()
349 |
350 | return {'lf_session': self.token}
351 |
352 | def __init__(self):
353 | self.load_data()
354 |
355 | if not self.is_actual:
356 | if self.create_new():
357 | self.save_data()
358 |
359 | def request(self, url, params=None, decode=True):
360 | args = [url]
361 |
362 | try:
363 | if proxy['enable'] and self.site_name in url:
364 | opener = request.build_opener(
365 | request.ProxyBasicAuthHandler(),
366 | request.ProxyHandler(proxy['proxy_urls'])
367 | )
368 |
369 | logger.info('proxy used for "%s"', url)
370 | else:
371 | opener = request.build_opener()
372 |
373 | # use cookies only for lostfilm site urls
374 | if self.site_name in url:
375 | if not params:
376 | params = self.cookies
377 | else:
378 | params.update(self.cookies)
379 |
380 | if params:
381 | args.append(parse.urlencode(params).encode('utf-8'))
382 |
383 | result = opener.open(*args).read()
384 |
385 | return result if not decode else result.decode('utf-8')
386 |
387 | except Exception as e:
388 | logger.error('%s url="%s" params="%s"' % (e, url, params))
389 |
390 | def load_data(self):
391 | if not os.path.exists(self.file_path):
392 | return
393 |
394 | with open(self.file_path, 'r') as file:
395 | result = json.load(file)
396 |
397 | if result.get('token') and result.get('time'):
398 | self.token = result['token']
399 | self.time = self.datetime_from_string(result['time'])
400 |
401 | logger.info('%s %s', self.token, self.time)
402 |
403 | def create_new(self):
404 | self._error = None
405 |
406 | if not (EMAIL and PASSWORD):
407 | self._error = 'Incorrect login data'
408 | logger.error(self._error)
409 | return False
410 |
411 | login_data = {
412 | "act": "users",
413 | "type": "login",
414 | "mail": EMAIL,
415 | "pass": PASSWORD,
416 | "need_captcha": "",
417 | "captcha": "",
418 | "rem": 1
419 | }
420 |
421 | url = SITE_URL + '/ajaxik.php?'
422 | params = parse.urlencode(login_data).encode('utf-8')
423 |
424 | cjar = CookieJar()
425 | if proxy['enable']:
426 | opener = request.build_opener(
427 | request.ProxyHandler(proxy['proxy_urls']),
428 | request.HTTPCookieProcessor(cjar)
429 | )
430 | logger.debug('proxy used')
431 | else:
432 | opener = request.build_opener(request.HTTPCookieProcessor(cjar))
433 |
434 | try:
435 | response = opener.open(url, params).read().decode('utf-8')
436 | except Exception as e:
437 | self._error = 'Connection failed'
438 | logger.error('%s %s', self._error, e)
439 | return False
440 |
441 | result = json.loads(response)
442 |
443 | if 'error' in result:
444 | self._error = 'Incorrect login data'
445 |
446 | elif 'need_captcha' in result:
447 | self._error = 'Captcha requested'
448 |
449 | else:
450 | for cookie in cjar:
451 | if cookie.name == 'lf_session':
452 | self.time = datetime.now()
453 | self.token = cookie.value
454 |
455 | logger.info('%s %s', self.token, self.time)
456 |
457 | return True
458 |
459 | else:
460 | self._error = 'Token problem'
461 |
462 | logger.error(self._error)
463 |
464 | return False
465 |
466 | def save_data(self):
467 | data = {
468 | "token": self.token,
469 | "time": None if not self.time else self.datetime_to_string(self.time)
470 | }
471 |
472 | logger.info(data)
473 |
474 | with open(self.file_path, 'w') as file:
475 | json.dump(data, file)
476 |
477 | def datetime_to_string(self, dt_obj):
478 | if isinstance(dt_obj, datetime):
479 | return dt_obj.strftime(self.datetime_format)
480 |
481 | else:
482 | raise TypeError('argument must be datetime, not %s' % (type(dt_obj)))
483 |
484 | def datetime_from_string(self, dt_string):
485 | if isinstance(dt_string, str):
486 | return datetime.strptime(dt_string, self.datetime_format)
487 |
488 | else:
489 | raise TypeError('argument must be str, not %s' % (type(dt_string)))
490 |
491 |
492 | class Tag:
493 | def __init__(self, tag=None, attrs=(), is_self_closing=None):
494 | self.type = tag
495 | self.is_self_closing = is_self_closing
496 | self._attrs = tuple(attrs)
497 | self._content = tuple()
498 |
499 | @property
500 | def attrs(self):
501 | """returns dict of Tag's attrs"""
502 | return dict(self._attrs)
503 |
504 | @property
505 | def text(self):
506 | """returns str of all contained text"""
507 | return ''.join(c if isinstance(c, str) else c.text for c in self._content)
508 |
509 | def _add_content(self, obj):
510 | if isinstance(obj, (Tag, str)):
511 | self._content += (obj,)
512 | else:
513 | raise TypeError('Argument must be str or %s, not %s' % (self.__class__, obj.__class__))
514 |
515 | def find(self, tag=None, attrs=None):
516 | """returns Tag or None"""
517 |
518 | return next(self._find_all(tag, attrs), None)
519 |
520 | def find_all(self, tag=None, attrs=None):
521 | """returns list"""
522 |
523 | return list(self._find_all(tag, attrs))
524 |
525 | def _find_all(self, tag_type=None, attrs=None):
526 | """returns generator"""
527 |
528 | if not (isinstance(tag_type, (str, Tag)) or tag_type is None):
529 | raise TypeError('tag_type argument must be str or Tag, not %s' % (tag_type.__class__))
530 |
531 | if not (isinstance(attrs, dict) or attrs is None):
532 | raise TypeError('attrs argument must be dict, not %s' % (self.__class__))
533 |
534 | # get tags-descendants generator
535 | results = self.descendants
536 |
537 | # filter by Tag.type
538 | if tag_type:
539 | if isinstance(tag_type, Tag):
540 | tag_type, attrs = tag_type.type, (attrs if attrs else tag_type.attrs)
541 |
542 | results = filter(lambda t: t.type == tag_type, results)
543 |
544 | # filter by Tag.attrs
545 | if attrs:
546 | # remove Tags without attrs
547 | results = filter(lambda t: t._attrs, results)
548 |
549 | def filter_func(tag):
550 | for key in attrs.keys():
551 | if attrs[key] not in tag.attrs.get(key, ()):
552 | return False
553 | return True
554 |
555 | # filter by attrs
556 | results = filter(filter_func, results)
557 |
558 | yield from results
559 |
560 | @property
561 | def children(self):
562 | """returns generator of tags-children"""
563 |
564 | return (obj for obj in self._content if isinstance(obj, Tag))
565 |
566 | @property
567 | def descendants(self):
568 | """returns generator of tags-descendants"""
569 |
570 | for child_tag in self.children:
571 | yield child_tag
572 | yield from child_tag.descendants
573 |
574 | def __getitem__(self, key):
575 | return self.attrs[key]
576 |
577 | def __getattr__(self, attr):
578 | if not attr.startswith("__"):
579 | return self.find(tag=attr)
580 |
581 | def __repr__(self):
582 | attrs = ' '.join(str(k) if v is None else '{}="{}"'.format(k, v) for k, v in self._attrs)
583 | starttag = ' '.join((self.type, attrs)) if attrs else self.type
584 |
585 | if self.is_self_closing:
586 | return '<{}>\n'.format(starttag)
587 | else:
588 | nested = '\n' * bool(next(self.children, None)) + ''.join(map(str, self._content))
589 | return '<{}>{}{}>\n'.format(starttag, nested, self.type)
590 |
591 |
592 | class Parser(HTMLParser):
593 | def __init__(self, html_code, *args, **kwargs):
594 | super().__init__(*args, **kwargs)
595 |
596 | self._root = Tag('_root')
597 | self._path = [self._root]
598 |
599 | self.feed(''.join(map(str.strip, html_code.splitlines())))
600 | self.handle_endtag(self._root.type)
601 | self.close()
602 |
603 | self.find = self._root.find
604 | self.find_all = self._root.find_all
605 |
606 | @property
607 | def attrs(self):
608 | return self._root.attrs
609 |
610 | @property
611 | def text(self):
612 | return self._root.text
613 |
614 | def handle_starttag(self, tag, attrs):
615 | self._path.append(Tag(tag=tag, attrs=attrs))
616 |
617 | def handle_endtag(self, tag_type):
618 | for pos, tag in tuple(enumerate(self._path))[::-1]:
619 | if isinstance(tag, Tag) and tag.type == tag_type and tag.is_self_closing is None:
620 | tag.is_self_closing = False
621 |
622 | for obj in self._path[pos + 1:]:
623 | if isinstance(obj, Tag) and obj.is_self_closing is None:
624 | obj.is_self_closing = True
625 |
626 | tag._add_content(obj)
627 |
628 | self._path = self._path[:pos + 1]
629 |
630 | break
631 |
632 | def handle_startendtag(self, tag, attrs):
633 | self._path.append(Tag(tag=tag, attrs=attrs, is_self_closing=True))
634 |
635 | def handle_decl(self, decl):
636 | self._path.append(Tag(tag='!'+decl, is_self_closing=True))
637 |
638 | def handle_data(self, text):
639 | self._path.append(text)
640 |
641 | def __getitem__(self, key):
642 | return self.attrs[key]
643 |
644 | def __getattr__(self, attr):
645 | if not attr.startswith("__"):
646 | return getattr(self._root, attr)
647 |
648 | def __repr__(self):
649 | return ''.join(str(c) for c in self._root._content)
650 |
651 |
652 | def bencode(value):
653 | if isinstance(value, dict):
654 | return b'd%be' % b''.join([bencode(k) + bencode(v) for k, v in value.items()])
655 | if isinstance(value, list) or isinstance(value, tuple):
656 | return b'l%be' % b''.join([bencode(v) for v in value])
657 | if isinstance(value, int):
658 | return b'i%ie' % value
659 | if isinstance(value, bytes):
660 | return b'%i:%b' % (len(value), value)
661 |
662 | raise ValueError("Only int, bytes, list or dict can be encoded, got %s" % type(value).__name__)
663 |
664 |
665 | def bdecode(data):
666 | class InvalidBencode(Exception):
667 | @classmethod
668 | def at_position(cls, error, position):
669 | logger.error("%s at position %i" % (error, position))
670 | return cls("%s at position %i" % (error, position))
671 |
672 | @classmethod
673 | def eof(cls):
674 | logger.error("EOF reached while parsing")
675 | return cls("EOF reached while parsing")
676 |
677 | def decode_from_io(f):
678 | char = f.read(1)
679 | if char == b'd':
680 | dict_ = OrderedDict()
681 | while True:
682 | position = f.tell()
683 | char = f.read(1)
684 | if char == b'e':
685 | return dict_
686 | if char == b'':
687 | raise InvalidBencode.eof()
688 |
689 | f.seek(position)
690 | key = decode_from_io(f)
691 | dict_[key] = decode_from_io(f)
692 |
693 | if char == b'l':
694 | list_ = []
695 | while True:
696 | position = f.tell()
697 | char = f.read(1)
698 | if char == b'e':
699 | return list_
700 | if char == b'':
701 | raise InvalidBencode.eof()
702 | f.seek(position)
703 | list_.append(decode_from_io(f))
704 |
705 | if char == b'i':
706 | digits = b''
707 | while True:
708 | char = f.read(1)
709 | if char == b'e':
710 | break
711 | if char == b'':
712 | raise InvalidBencode.eof()
713 | if not char.isdigit():
714 | raise InvalidBencode.at_position('Expected int, got %s' % str(char), f.tell())
715 | digits += char
716 | return int(digits)
717 |
718 | if char.isdigit():
719 | digits = char
720 | while True:
721 | char = f.read(1)
722 | if char == b':':
723 | break
724 | if char == b'':
725 | raise InvalidBencode
726 | digits += char
727 | length = int(digits)
728 | string = f.read(length)
729 | return string
730 |
731 | raise InvalidBencode.at_position('Unknown type : %s' % char, f.tell())
732 |
733 | return decode_from_io(BytesIO(data))
734 |
735 |
736 | if __name__ == '__main__':
737 | import sys
738 |
739 | if 1 < len(sys.argv) < 4:
740 |
741 | if len(sys.argv) == 3:
742 | if sys.argv[1] == '-d':
743 | logger.setLevel(logging.DEBUG)
744 |
745 | else:
746 | print('%s [-d] "search query"' % (__file__))
747 | exit()
748 | else:
749 | logger.setLevel(logging.INFO)
750 |
751 | lostfilm(True).search(sys.argv[-1])
752 |
--------------------------------------------------------------------------------