默认的nexusPHP
插件虽然支持frds
,但是不支持Neutral
种子。只需要稍微修改下,即可。
下面是完整文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 |
# coding=utf-8 from __future__ import unicode_literals, division, absolute_import import time from builtins import * import concurrent.futures import re import logging from datetime import datetime from requests.adapters import HTTPAdapter from flexget import plugin from flexget.config_schema import one_or_more from flexget.event import event from flexget.utils.soup import get_soup from flexget.utils.tools import parse_timedelta log = logging.getLogger('nexusphp') class NexusPHP(object): """ 配置示例 task_name: rss: url: https://www.example.com/rss.xml other_fields: - link nexusphp: cookie: 'my_cookie' discount: - free - 2x seeders: min: 1 max: 30 leechers: min: 1 max: 100 max_complete: 0.8 hr: no """ schema = { 'type': 'object', 'properties': { 'cookie': {'type': 'string'}, 'discount': one_or_more({'type': 'string', 'enum': ['free', '2x', '2xfree', '30%', '50%', '2x50%', 'Neutral']}), 'seeders': { 'type': 'object', 'properties': { 'min': {'type': 'integer', 'minimum': 0, 'default': 0}, 'max': {'type': 'integer', 'minimum': 0, 'default': 100000} } }, 'leechers': { 'type': 'object', 'properties': { 'min': {'type': 'integer', 'minimum': 0, 'default': 0}, 'max': {'type': 'integer', 'minimum': 0, 'default': 100000}, 'max_complete': {'type': 'number', 'minimum': 0, 'maximum': 1, 'default': 1} } }, 'left-time': {'type': 'string', 'format': 'interval'}, 'hr': {'type': 'boolean'}, 'adapter': { 'type': 'object', 'properties': { 'free': {'type': 'string', 'default': 'free'}, '2x': {'type': 'string', 'default': 'twoup'}, '2xfree': {'type': 'string', 'default': 'twoupfree'}, '30%': {'type': 'string', 'default': 'thirtypercent'}, '50%': {'type': 'string', 'default': 'halfdown'}, '2x50%': {'type': 'string', 'default': 'twouphalfdown'}, 'Neutral': {'type': 'string', 'default': 'nl'} } }, 'comment': {'type': 'boolean'}, 'user-agent': {'type': 'string'}, 'remember': {'type': 'boolean', 'default': True} }, 'required': ['cookie'] } @staticmethod def build_config(config): config = dict(config) config.setdefault('discount', None) config.setdefault('seeders', None) config.setdefault('leechers', None) config.setdefault('left-time', None) config.setdefault('hr', True) config.setdefault('adapter', None) config.setdefault('user-agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko)' 'Chrome/89.0.4389.72 Safari/537.36 Edg/89.0.774.45') return config @plugin.priority(127) def on_task_modify(self, task, config): if config.get('comment', False): for entry in task.entries: if 'torrent' in entry and 'link' in entry: entry['torrent'].content['comment'] = entry['link'] entry['torrent'].modified = True def on_task_filter(self, task, config): config = self.build_config(config) adapter = HTTPAdapter(max_retries=5) task.requests.mount('http://', adapter) task.requests.mount('https://', adapter) task.requests.headers.update({ 'cookie': config['cookie'], 'user-agent': config['user-agent'] }) # 先访问一次 预防异常 try: task.requests.get(task.entries[0].get('link')) except Exception: pass def consider_entry(_entry, _link): try: discount, seeders, leechers, hr, expired_time = NexusPHP._get_info(task, _link, config) except plugin.PluginError as e: raise e except Exception as e: log.info('NexusPHP._get_info: ' + str(e)) return remember = config['remember'] if config['discount']: if discount not in config['discount']: _entry.reject('%s does not match discount' % discount, remember=remember) # 优惠信息不匹配 return if config['left-time'] and expired_time: left_time = expired_time - datetime.now() # 实际剩余时间 < 'left-time' if left_time < parse_timedelta(config['left-time']): _entry.reject('its discount time only left [%s]' % left_time, remember=remember) # 剩余时间不足 return if config['hr'] is False and hr: _entry.reject('it is HR', remember=True) # 拒绝HR if config['seeders']: seeder_max = config['seeders']['max'] seeder_min = config['seeders']['min'] if len(seeders) not in range(seeder_min, seeder_max + 1): _entry.reject('%d is out of range of seeder' % len(seeders), remember=True) # 做种人数不匹配 return if config['leechers']: leecher_max = config['leechers']['max'] leecher_min = config['leechers']['min'] if len(leechers) not in range(leecher_min, leecher_max + 1): _entry.reject('%d is out of range of leecher' % len(leechers), remember=True) # 下载人数不匹配 return if len(leechers) != 0: max_complete = max(leechers, key=lambda x: x['completed'])['completed'] else: max_complete = 0 if max_complete > config['leechers']['max_complete']: _entry.reject('%f is more than max_complete' % max_complete, remember=True) # 最大完成度不匹配 return _entry.accept() futures = [] # 线程任务 with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: for entry in task.accepted + task.undecided: link = entry.get('link') if not link: raise plugin.PluginError("The rss plugin require 'other_fields' which contain 'link'. " "For example: other_fields: - link") futures.append(executor.submit(consider_entry, entry, link)) time.sleep(0.5) for f in concurrent.futures.as_completed(futures): exception = f.exception() if isinstance(exception, plugin.PluginError): log.error(exception) @staticmethod # 解析页面,获取优惠、做种者信息、下载者信息 def info_from_page(detail_page, peer_page, discount_fn, hr_fn): try: discount, expired_time = discount_fn(detail_page) except plugin.PluginError as e: raise e except Exception: discount = expired_time = None # 无优惠 try: if hr_fn: hr = hr_fn(detail_page) else: hr = False for item in ['hitandrun', 'hit_run.gif', 'Hit and Run', 'Hit & Run']: if item in detail_page.text: hr = True break except plugin.PluginError as e: raise e except Exception: hr = False # 无HR soup = get_soup(peer_page.replace('\n', ''), 'html5lib') seeders = leechers = [] tables = soup.find_all('table', limit=2) if len(tables) == 2: # 1. seeder leecher 均有 seeders = NexusPHP.get_peers(tables[0]) leechers = NexusPHP.get_peers(tables[1]) elif len(tables) == 1 and len(soup.body.contents) == 3: # 2. seeder leecher 有其一 nodes = soup.body.contents if nodes[1].name == 'table': # 2.1 只有seeder 在第二个节点 seeders = NexusPHP.get_peers(nodes[1]) else: # 2.2 只有leecher 在第三个节点 leechers = NexusPHP.get_peers(nodes[2]) else: # 3. seeder leecher 均无 seeders = leechers = [] return discount, seeders, leechers, hr, expired_time @staticmethod def get_peers(table): peers = [] name_index = 0 connectable_index = 1 uploaded_index = 2 downloaded_index = 4 completed_index = 7 for index, tr in enumerate(table.find_all('tr')): try: if index == 0: tds = tr.find_all('td') for i, td in enumerate(tds): text = td.get_text() if text in ['用户', '用戶', '会员/IP']: name_index = i elif text in ['可连接', '可連接', '公网']: connectable_index = i elif text in ['上传', '上傳', '总上传']: uploaded_index = i elif text in ['下载', '下載', '本次下载']: downloaded_index = i elif text == '完成': completed_index = i else: tds = tr.find_all('td') peers.append({ 'name': tds[name_index].get_text(), 'connectable': True if tds[connectable_index].get_text() != '是' else False, 'uploaded': tds[uploaded_index].get_text(), 'downloaded': tds[downloaded_index].get_text(), 'completed': float(tds[completed_index].get_text().strip('%')) / 100 }) except Exception: peers.append({ 'name': '', 'connectable': False, 'uploaded': '', 'downloaded': '', 'completed': 0 }) return peers @staticmethod def _get_info(task, link, config): if 'open.cd' in link: link = link.replace('details.php', 'plugin_details.php') detail_page = task.requests.get(link, timeout=20) # 详情 detail_page.encoding = 'utf-8' if 'login' in detail_page.url or 'portal.php' in detail_page.url: raise plugin.PluginError("Can't access the site. Your cookie may be wrong!") # 1. peer page def get_peer_page(): if 'totheglory' in link: return '' if 'lemonhd' in link: # https://lemonhd.org/details_movie.php?id=xxx # https://lemonhd.org/details_music.php?id=xxx # ... peer_url = re.sub(r'details_\w+.php', 'viewpeerlist.php', link, 1) else: peer_url = link.replace('details.php', 'viewpeerlist.php', 1) try: if config['seeders'] or config['leechers']: # 配置了seeders、leechers才请求 return task.requests.get(peer_url).text # peer详情 except Exception: return '' return '' peer_page = get_peer_page() # 2. HR hr_fn = None if 'chdbits' in link: def chd_hr_fn(_page): if '<b>H&R' in _page.text: return True return False hr_fn = chd_hr_fn # 3. discount sites_discount = { 'chdbits': { 'pro_free2up.*?</h1>': '2xfree', 'pro_free.*?</h1>': 'free', 'pro_2up.*?</h1>': '2x', 'pro_50pctdown2up.*?</h1>': '2x50%', 'pro_30pctdown.*?</h1>': '30%', 'pro_50pctdown.*?</h1>': '50%' }, 'u2.dmhy': { 'class=.pro_2up.*?promotion.*?</td>': '2x', 'class=.pro_free2up.*?promotion.*?</td>': '2xfree', 'class=.pro_free.*?promotion.*?</td>': 'free', 'class=.pro_50pctdown2up.*?promotion.*?</td>': '2x50%', 'class=.pro_30pctdown.*?promotion.*?</td>': '30%', 'class=.pro_50pctdown.*?promotion.*?</td>': '50%', r'class=.pro_custom.*?0\.00X.*?promotion.*?</td>': '2xfree' }, 'totheglory': { '本种子限时不计流量.*?</font>': 'free', '本种子的下载流量计为实际流量的30%.*?</font>': '30%', '本种子的下载流量会减半.*?</font>': '50%', }, 'open.cd': { 'pro_free2up': '2xfree', 'pro_free': 'free', 'pro_2up': '2x', 'pro_50pctdown2up': '2x50%', 'pro_30pctdown': '30%', 'pro_50pctdown': '50%' } } discount_fn = NexusPHP.generate_discount_fn({ 'class=\'free\'.*?免.*?</h1>': 'free', 'class=\'twoup\'.*?2X.*?</h1>': '2x', 'class=\'twoupfree\'.*?2X免.*?</h1>': '2xfree', 'class=\'thirtypercent\'.*?30%.*?</h1>': '30%', 'class=\'halfdown\'.*?50%.*?</h1>': '50%', 'class=\'twouphalfdown\'.*?2X 50%.*?</h1>': '2x50%', 'class=\'nl\'.*?中性.*?</h1>': 'Neutral', }) for site, convert in sites_discount.items(): if site in link: discount_fn = NexusPHP.generate_discount_fn(convert) break if 'hdchina' in link: def _(page): return NexusPHP.get_discount_from_hdchina(page, task) discount_fn = _ if config['adapter']: convert = {value: key for key, value in config['adapter'].items()} discount_fn = NexusPHP.generate_discount_fn(convert) return NexusPHP.info_from_page(detail_page, peer_page, discount_fn, hr_fn) @staticmethod def get_discount_from_hdchina(details_page, task): soup = get_soup(details_page.text, 'html5lib') csrf = soup.find('meta', attrs={'name': 'x-csrf'})['content'] torrent_id = str(soup.find('div', class_='details_box').find('span', class_='sp_state_placeholder')['id']) res = task.requests.post('https://hdchina.org/ajax_promotion.php', data={ 'ids[]': torrent_id, 'csrf': csrf, }, timeout=10) """ sample response { 'status': 200, 'message': { '530584': { 'sp_state': '<p style="display: none"> <img class="pro_free" src="pic/trans.gif" alt="Free" onmouseover="domTT_activate(this, event, \'content\', \'<b><font class="free">免费</font></b>', 'timeout': '' } } } """ if res.status_code != 200: return None, None res = res.json() if res['status'] != 200: return None, None discount_info = res['message'][torrent_id] if 'sp_state' not in discount_info or not discount_info['sp_state']: return None, None if '<p style="display: none">' in discount_info['sp_state']: # HDC cookie 仅部分错误时会直接返回free # 同时带有特征 <p style="display: none"> # 也许是站点的BUG raise plugin.PluginError("Can't access the site. Your cookie may be wrong!") expired_time = None match = re.search(r'(\d{4})(-\d{1,2}){2}\s\d{1,2}(:\d{1,2}){2}', discount_info['timeout']) if match: expired_time_str = match.group(0) expired_time = datetime.strptime(expired_time_str, "%Y-%m-%d %H:%M:%S") discount_mapping = { 'class="pro_free2up"': '2xfree', 'class="pro_free"': 'free', 'class="pro_2up"': '2x', 'class="pro_50pctdown2up"': '2x50%', 'class="pro_30pctdown"': '30%', 'class="pro_50pctdown"': '50%' } for key, value in discount_mapping.items(): if key in discount_info['sp_state']: return value, expired_time return None, None @staticmethod def generate_discount_fn(convert): def fn(page): html = page.text.replace('\n', '') for key, value in convert.items(): match = re.search(key, html) if match: discount_str = match.group(0) expired_time = None # 匹配优惠剩余时间 match = re.search(r'(\d{4})(-\d{1,2}){2}\s\d{1,2}(:\d{1,2}){2}', discount_str) if match: expired_time_str = match.group(0) expired_time = datetime.strptime(expired_time_str, "%Y-%m-%d %H:%M:%S") return value, expired_time return None, None return fn @event('plugin.register') def register_plugin(): plugin.register(NexusPHP, 'nexusphp', api_ver=2) |
只需要在配置文件中加上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
frds: rss: url: https://pt.xxxxxx other_fields: - link accept_all: yes download: /Users/mcj/Downloads/CCC不备份/flexget nexusphp: cookie: 'xxxx' hr: no discount: - free - 2xfree - Neutral |
本文最后更新于2021年10月31日,已超过 1 年没有更新,如果文章内容或图片资源失效,请留言反馈,我们会及时处理,谢谢!